package org.tiki.tikitoken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.sasl.Sasl; import javax.security.sasl.SaslException; import javax.security.sasl.SaslServer; import java.nio.charset.StandardCharsets; import java.util.StringTokenizer; /** * A SaslServer implementation of the TikiToken mechanism. * * @author Guus der Kinderen, guus@goodbytes.nl */ public class TikiTokenSaslServer implements SaslServer { private final static Logger Log = LoggerFactory.getLogger( TikiTokenSaslServer.class ); /** * The mechanism name of this implementation: "TIKITOKEN" */ public static final String MECHANISM_NAME = "TIKITOKEN"; private enum State { /** Initial state. Has not evaluated any response yet. */ PRE_INITIAL_RESPONSE, /** Has evaluated an initial response, but has not yet completed. */ POST_INITIAL_RESPONSE, /** Done (authentication succeeded or failed). */ COMPLETED } private String authorizationID = null; private State state = State.PRE_INITIAL_RESPONSE; public TikiTokenSaslServer() {} /** * Returns the mechanism name of this SASL server: "TIKITOKEN". * * @return A non-null string representing the mechanism name: TIKITOKEN */ public String getMechanismName() { return MECHANISM_NAME; } public byte[] evaluateResponse( byte[] response ) throws SaslException { Log.trace( "Evaluating new response..." ); if( isComplete() ) { throw new IllegalStateException( "TIKITOKEN authentication was already completed." ); } Log.trace( "Current state: {}", state ); switch ( state ) { case POST_INITIAL_RESPONSE: if ( response.length == 0 ) { state = State.COMPLETED; throw new SaslException( "The TIKITOKEN SASL mechanism expects response data in either the initial or second client response. Neither had any data." ); } // Intended fall-through. case PRE_INITIAL_RESPONSE: if ( response.length == 0 ) { // No data in the initial response. Ask for data by responding with a success. state = State.POST_INITIAL_RESPONSE; return null; } else { // We have data: no further responses are expected. state = State.COMPLETED; Log.trace( "Parsing data from client response..." ); final String data = new String( response, StandardCharsets.UTF_8); final StringTokenizer tokens = new StringTokenizer( data, "\0"); if ( tokens.countTokens() != 2 ) { throw new SaslException( "Exactly two NUL (U+0000) character-separated values are expected (a username, followed by a Tiki access token). Instead " + tokens.countTokens() + " were found." ); } final String username = tokens.nextToken(); final String tikiToken = tokens.nextToken(); Log.trace( "Parsed data from client response for user '{}'. Verifying Tiki token...", username ); final TikiTokenQuery query = new TikiTokenQuery( username, tikiToken ); if ( !query.isValid() ) { throw new SaslException( "Tiki token based authentication failed for: " + username ); } Log.debug( "Authentication successful for user '{}'!", username ); authorizationID = username; return null; } default: throw new IllegalStateException( "Instance is in an unrecognized state (please report this incident as a bug in class: " + this.getClass().getCanonicalName() + "). Unrecognized value: " + state ); } } public boolean isComplete() { return state == State.COMPLETED; } public String getAuthorizationID() { if( !isComplete() ) { throw new IllegalStateException( "TIKITOKEN authentication has not completed." ); } return authorizationID; } public Object getNegotiatedProperty( String propName ) { if( !isComplete() ) { throw new IllegalStateException( "TIKITOKEN authentication has not completed." ); } if ( Sasl.QOP.equals( propName ) ) { return "auth"; } return null; } public void dispose() throws SaslException { state = null; authorizationID = null; } public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException { if( !isComplete() ) { throw new IllegalStateException( "TIKITOKEN authentication has not completed." ); } throw new IllegalStateException( "TIKITOKEN supports neither integrity nor privacy." ); } public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException { if( !isComplete() ) { throw new IllegalStateException( "TIKITOKEN authentication has not completed." ); } throw new IllegalStateException( "TIKITOKEN supports neither integrity nor privacy." ); } }