package org.jivesoftware.openfire.plugin.ofmeet; import org.jivesoftware.util.Base64; import java.security.Principal; import java.security.SecureRandom; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A manager of access tokens for a user. The generated access token is cryptographically strong. * * This class is intended to be used as a singleton. The instance is thread-safe. * * @author Guus der Kinderen, guus@goodbytes.nl */ // TODO Tokens remain active until Jetty logs out a principal. I'm not sure if that happens when sessions implicitly end (eg timeout). Perhaps these tokens should have a limited time-span. public class TokenManager { private static final Logger Log = LoggerFactory.getLogger( TokenManager.class ); private static TokenManager instance = null; protected final Map<Principal, Set<String>> userToToken = new HashMap<>(); protected final Map<String, Principal> tokenToUser = new HashMap<>(); private final SecureRandom random = new SecureRandom(); public synchronized static TokenManager getInstance() { if ( instance == null ) { instance = new TokenManager(); } return instance; } /** * Generates a new access token. * * The token is guaranteed not to be used at the time of invocation. * * @return a base64 encoded, unique access token (never null, never empty) */ protected synchronized String generateToken() { String encoded; do { final byte bytes[] = new byte[ 40 ]; random.nextBytes( bytes ); encoded = Base64.encodeBytes( bytes ); if ( encoded == null || encoded.isEmpty() ) { throw new IllegalStateException( "Generated a null or empty random Base64 String?" ); } } while ( tokenToUser.containsKey( encoded ) ); return encoded; } /** * Ensures that an access token is registered for the user. * * Invoking this method is a thread-safe alternative to the following: * <pre>{@code * String token = retrieveToken( identity ); * if (token == null) { * token = registerNewToken( identity ); * } * return token; * }</pre> * * @param identity A user identity (cannot be null). * @return an access token for the user (never null). */ public synchronized String registerIfAbsent( Principal identity ) { String token = retrieveToken( identity ); if (token == null) { token = registerNewToken( identity ); } return token; } /** * Adds a new access token for a user. * * If a token was previously registered for this user, the old value is discarded. * * @param identity A user identity (cannot be null). * @return a new access token for the user (never null). */ public synchronized String registerNewToken( Principal identity ) { final String token = generateToken(); Set<String> tokens = userToToken.get( identity ); if ( tokens == null ) { tokens = new HashSet<>(); } tokens.add( token ); userToToken.put( identity, tokens ); tokenToUser.put( token, identity ); Log.warn("storing local token " + token + "\n" + identity); return token; } /** * Adds a new azure access token for a user. * * If a token was previously registered for this user, the old value is discarded. * * @param identity A user identity (cannot be null). * @param new access token for the user (cannot be null). */ public synchronized void registerNewAzureToken( Principal identity, String token ) { Set<String> tokens = new HashSet<>(); tokens.add( token ); userToToken.put( identity, tokens ); tokenToUser.put( token, identity ); Log.warn("storing azure token " + token + "\n" + identity); } /** * Verifies if an access token is currently registered for a user. * * @param identity A user identity (cannot be null). * @return true when an access token for the user is currently active, otherwise false. */ public synchronized boolean containsToken( Principal identity ) { return userToToken.containsKey( identity ); } /** * Retrieves an access token for a user. * * No assumptions can be made which token is returned by this method, when more than one token has been registered * for the user. * * @param identity A user identity (cannot be null). * @return The access token, or null if no access token is registered for the user. */ public synchronized String retrieveToken( Principal identity ) { final Set<String> tokens = userToToken.get(identity); if ( tokens == null ) { return null; } return tokens.iterator().next(); } /** * Invalidates the access tokens for a user. * * If no access token was active for this user, an invocation of this method has no effect. * * @param identity A user identity (cannot be null). */ public synchronized void revoke( Principal identity ) { final Set<String> tokens = userToToken.remove( identity ); if ( tokens != null ) { for ( String token : tokens ) { tokenToUser.remove( token ); } } } /** * Returns the user that is identified by the provided token. * * @param token A token (not null) * @return A user when the token is currently valid, otherwise null. */ public synchronized Principal validate( String token ) { return tokenToUser.get( token ); } }