+ * Authentication is delegated to various implementations. + * By default, it is handled by Karaf's JAAS implementation, but it is possible + * to override it by using {@link #setAuthService(IAuthService)}. You will HAVE + * TO use this method if you run the REST services outside Karaf. + *
+ *+ * When the authentication succeeds, a token is generated by this class + * (a random UUID in fact). The token is stored by this class and associated + * with the login time. + *
+ *+ * Since sessions can be limited in time (depending on admin preferences), + * we can verify on every action that the session is still valid. + *
+ *+ * To prevent "man in the middle" "attacks, authentication should be + * used along with HTTPS. + *
+ * + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationManager { + + private final ConcurrentHashMap+ * No error is thrown if the session was already invalid. + *
+ * + * @param token a token + */ + public void logout( String token ) { + if( token != null ) + this.tokenToLoginTime.remove( token ); + } + + + /** + * An abstraction to manage authentication. + * @author Vincent Zurczak - Linagora + */ + public interface IAuthService { + + /** + * Authenticates someone by user and password. + * @param user a user name + * @param pwd a password + * @throws LoginException if authentication failed + */ + void authenticate( String user, String pwd ) throws LoginException; + + /** + * Sets the REALM to use. + * @param realm a realm name + */ + void setRealm( String realm ); + } + + + /** + * Authentication managed by Apache Karaf. + *+ * Karaf uses JAAS and by default supports several login modules + * (properties files, databases, LDAP, etc). + *
+ * @author Vincent Zurczak - Linagora + */ + public static class KarafAuthService implements IAuthService { + private String realm; + + + @Override + public void authenticate( String user, String pwd ) throws LoginException { + LoginContext loginCtx = new LoginContext( this.realm, new RoboconfCallbackHandler( user, pwd )); + loginCtx.login(); + } + + @Override + public void setRealm( String realm ) { + this.realm = realm; + } + } + + + /** + * A callback handler for JAAS. + * @author Vincent Zurczak - Linagora + */ + static final class RoboconfCallbackHandler implements CallbackHandler { + private final String username, password; + + + /** + * Constructor. + * @param username + * @param password + */ + public RoboconfCallbackHandler( String username, String password ) { + this.username = username; + this.password = password; + } + + + @Override + public void handle( Callback[] callbacks ) throws IOException, UnsupportedCallbackException { + + for( Callback callback : callbacks ) { + if (callback instanceof NameCallback ) + ((NameCallback) callback).setName( this.username ); + else if( callback instanceof PasswordCallback ) + ((PasswordCallback) callback).setPassword( this.password.toCharArray()); + else + throw new UnsupportedCallbackException( callback ); + } + } + } +} diff --git a/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java b/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java new file mode 100644 index 00000000..028632d0 --- /dev/null +++ b/core/roboconf-dm-rest-commons/src/test/java/net/roboconf/dm/rest/commons/security/AuthenticationManagerTest.java @@ -0,0 +1,134 @@ +/** + * Copyright 2017 Linagora, Université Joseph Fourier, Floralis + * + * The present code is developed in the scope of the joint LINAGORA - + * Université Joseph Fourier - Floralis research program and is designated + * as a "Result" pursuant to the terms and conditions of the LINAGORA + * - Université Joseph Fourier - Floralis research program. Each copyright + * holder of Results enumerated here above fully & independently holds complete + * ownership of the complete Intellectual Property rights applicable to the whole + * of said Results, and may freely exploit it in any manner which does not infringe + * the moral rights of the other copyright holders. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.roboconf.dm.rest.commons.security; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.LanguageCallback; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import net.roboconf.dm.rest.commons.security.AuthenticationManager.IAuthService; +import net.roboconf.dm.rest.commons.security.AuthenticationManager.RoboconfCallbackHandler; + +/** + * @author Vincent Zurczak - Linagora + */ +public class AuthenticationManagerTest { + + @Test + public void testAuthenticationChain_success() { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNotNull( token ); + Assert.assertTrue( mngr.isSessionValid( token, 1 )); + Assert.assertTrue( mngr.isSessionValid( token, -1 )); + + mngr.logout( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testAuthenticationChain_failure() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + Mockito.doThrow( new LoginException( "for test" )).when( authService ).authenticate( Mockito.anyString(), Mockito.anyString()); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNull( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + + mngr.logout( token ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testAuthenticationChain_validityPeriodExpired() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + IAuthService authService = Mockito.mock( IAuthService.class ); + mngr.setAuthService( authService ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNotNull( token ); + Assert.assertTrue( mngr.isSessionValid( token, 1 )); + Thread.sleep( 1020 ); + Assert.assertFalse( mngr.isSessionValid( token, 1 )); + + // The session was removed, it should not be marked as valid anymore + Assert.assertFalse( mngr.isSessionValid( token, 10 )); + } + + + @Test + public void testAuthenticationChain_withKaraf_butOutsideKaraf() throws Exception { + + AuthenticationManager mngr = new AuthenticationManager( "realm" ); + + String token = mngr.login( "me", "my password" ); + Assert.assertNull( token ); + Assert.assertFalse( mngr.isSessionValid( token, -1 )); + } + + + @Test + public void testRoboconfCallbackHandler_success() throws Exception { + + RoboconfCallbackHandler handler = new RoboconfCallbackHandler( "user", "password" ); + handler.handle( new Callback[] { + new NameCallback( "Username: " ), + new PasswordCallback( "Password: ", false ) + }); + } + + + @Test( expected = UnsupportedCallbackException.class ) + public void testRoboconfCallbackHandler_failure() throws Exception { + + RoboconfCallbackHandler handler = new RoboconfCallbackHandler( "user", "password" ); + handler.handle( new Callback[] { + new NameCallback( "Username: " ), + new PasswordCallback( "Password: ", false ), + new LanguageCallback() + }); + } +}