Skip to content

Commit

Permalink
roboconf#632 User authentication
Browse files Browse the repository at this point in the history
The hardest part is done, integrate with Karaf's JAAS implementation.
Next step: implement a servlet filter that uses it
  • Loading branch information
vincent-zurczak committed Mar 2, 2017
1 parent 4bd4bea commit 11b3c25
Show file tree
Hide file tree
Showing 3 changed files with 383 additions and 1 deletion.
8 changes: 7 additions & 1 deletion core/roboconf-dm-rest-commons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@
<artifactId>roboconf-dm</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/**
* 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 java.io.IOException;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

/**
* A class in charge of managing authentication and sessions.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* Since sessions can be limited in time (depending on admin preferences),
* we can verify on every action that the session is still valid.
* </p>
* <p>
* To prevent "man in the middle" "attacks, authentication should be
* used along with HTTPS.
* </p>
*
* @author Vincent Zurczak - Linagora
*/
public class AuthenticationManager {

private final ConcurrentHashMap<String,Long> tokenToLoginTime = new ConcurrentHashMap<> ();
private final Logger logger = Logger.getLogger( getClass().getName());
private final String realm;

private IAuthService authService;



/**
* Constructor.
* @param realm
*/
public AuthenticationManager( String realm ) {
this.realm = realm;
this.authService = new KarafAuthService();
this.authService.setRealm( this.realm );
}


/**
* @param authenticater the authService to set
*/
public void setAuthService( IAuthService authService ) {
this.authService = authService;
authService.setRealm( this.realm );
}


/**
* Authenticates a user and creates a new session.
* @param user a user name
* @param pwd a pass word
* @return a token if authentication worked, null if it failed
*/
public String login( String user, String pwd ) {

String token = null;
try {
this.authService.authenticate( user, pwd );

token = UUID.randomUUID().toString();
Long now = new Date().getTime();
this.tokenToLoginTime.put( token, now );

} catch( LoginException e ) {
this.logger.severe( "Invalid login attempt by user " + user );
}

return token;
}


/**
* Determines whether a session is valid.
* @param token a token
* @param validityPeriod the validity period for a session (in seconds)
* @return true if the session is valid, false otherwise
*/
public boolean isSessionValid( final String token, int validityPeriod ) {

boolean valid = false;
Long loginTime = null;
if( token != null )
loginTime = this.tokenToLoginTime.get( token );

if( validityPeriod < 0 ) {
valid = loginTime != null;

} else if( loginTime != null ) {
long now = new Date().getTime();
valid = (now - loginTime) <= validityPeriod * 1000;

// Invalid sessions should be deleted
if( ! valid )
logout( token );
}

return valid;
}


/**
* Invalidates a session.
* <p>
* No error is thrown if the session was already invalid.
* </p>
*
* @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.
* <p>
* Karaf uses JAAS and by default supports several login modules
* (properties files, databases, LDAP, etc).
* </p>
* @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 );
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
});
}
}

0 comments on commit 11b3c25

Please sign in to comment.