/** * 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 ConcurrentHashMap<String,String> tokenToUsername = 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 ); this.tokenToUsername.put( token, user ); } 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, < 0 for unbound) * @return true if the session is valid, false otherwise */ public boolean isSessionValid( final String token, long 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 (can be null) */ public void logout( String token ) { if( token != null ) { this.tokenToLoginTime.remove( token ); this.tokenToUsername.remove( token ); } } /** * Finds the user name associated with a given token (e.g. for audit). * @param token a token * @return a user name, or null if the otken did not match anything */ public String findUsername( String token ) { return token == null ? null : this.tokenToUsername.get( 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 ); } } } }