SASLAuthentication.java

/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * 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 org.jivesoftware.smack;

import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.sasl.SASLAnonymous;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;

import javax.security.auth.callback.CallbackHandler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * <p>This class is responsible authenticating the user using SASL, binding the resource
 * to the connection and establishing a session with the server.</p>
 *
 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
 * register with the server or authenticate using SASL. If the
 * server supports SASL then Smack will try to authenticate using SASL..</p>
 *
 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
 * {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms.
 *
 * @see org.jivesoftware.smack.sasl.SASLMechanism
 *
 * @author Gaston Dombiak
 * @author Jay Kline
 */
public class SASLAuthentication {

    private static final Logger LOGGER = Logger.getLogger(SASLAuthentication.class.getName());

    private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<SASLMechanism>();

    private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<String>();

    /**
     * Registers a new SASL mechanism
     *
     * @param mechanism a SASLMechanism subclass.
     */
    public static void registerSASLMechanism(SASLMechanism mechanism) {
        synchronized (REGISTERED_MECHANISMS) {
            REGISTERED_MECHANISMS.add(mechanism);
            Collections.sort(REGISTERED_MECHANISMS);
        }
    }

    /**
     * Returns the registered SASLMechanism sorted by the level of preference.
     *
     * @return the registered SASLMechanism sorted by the level of preference.
     */
    public static Map<String, String> getRegisterdSASLMechanisms() {
        Map<String, String> answer = new HashMap<String, String>();
        synchronized (REGISTERED_MECHANISMS) {
            for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
                answer.put(mechanism.getClass().getName(), mechanism.getName());
            }
        }
        return answer;
    }

    /**
     * Unregister a SASLMechanism by it's full class name. For example
     * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
     * 
     * @param clazz the SASLMechanism class's name
     * @return true if the given SASLMechanism was removed, false otherwise
     */
    public static boolean unregisterSASLMechanism(String clazz) {
        synchronized (REGISTERED_MECHANISMS) {
            Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
            while (it.hasNext()) {
                SASLMechanism mechanism = it.next();
                if (mechanism.getClass().getName().equals(clazz)) {
                    it.remove();
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean blacklistSASLMechanism(String mechansim) {
        synchronized(BLACKLISTED_MECHANISMS) {
            return BLACKLISTED_MECHANISMS.add(mechansim);
        }
    }

    public static boolean unBlacklistSASLMechanism(String mechanism) {
        synchronized(BLACKLISTED_MECHANISMS) {
            return BLACKLISTED_MECHANISMS.remove(mechanism);
        }
    }

    public static Set<String> getBlacklistedSASLMechanisms() {
        synchronized(BLACKLISTED_MECHANISMS) {
            return new HashSet<String>(BLACKLISTED_MECHANISMS);
        }
    }

    private final AbstractXMPPConnection connection;
    private SASLMechanism currentMechanism = null;

    /**
     * Boolean indicating if SASL negotiation has finished and was successful.
     */
    private boolean authenticationSuccessful;

    /**
     * Either of type {@link SmackException} or {@link SASLErrorException}
     */
    private Exception saslException;

    SASLAuthentication(AbstractXMPPConnection connection) {
        this.connection = connection;
        this.init();
    }

    /**
     * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
     *
     * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
     */
    public boolean hasAnonymousAuthentication() {
        return serverMechanisms().contains("ANONYMOUS");
    }

    /**
     * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
     *
     * @return true if the server offered SASL authentication besides ANONYMOUS SASL.
     */
    public boolean hasNonAnonymousAuthentication() {
        return !serverMechanisms().isEmpty() && (serverMechanisms().size() != 1 || !hasAnonymousAuthentication());
    }

    /**
     * Performs SASL authentication of the specified user. If SASL authentication was successful
     * then resource binding and session establishment will be performed. This method will return
     * the full JID provided by the server while binding a resource to the connection.<p>
     *
     * The server may assign a full JID with a username or resource different than the requested
     * by this method.
     *
     * @param resource the desired resource.
     * @param cbh the CallbackHandler used to get information from the user
     * @throws IOException 
     * @throws XMPPErrorException 
     * @throws SASLErrorException 
     * @throws SmackException 
     * @throws InterruptedException 
     */
    public void authenticate(String resource, CallbackHandler cbh) throws IOException,
                    XMPPErrorException, SASLErrorException, SmackException, InterruptedException {
        SASLMechanism selectedMechanism = selectMechanism();
        if (selectedMechanism != null) {
            currentMechanism = selectedMechanism;
            synchronized (this) {
                currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh);
                // Wait until SASL negotiation finishes
                wait(connection.getPacketReplyTimeout());
            }

            maybeThrowException();

            if (!authenticationSuccessful) {
                throw NoResponseException.newWith(connection);
            }
        }
        else {
            throw new SmackException(
                            "SASL Authentication failed. No known authentication mechanisims.");
        }
    }

    /**
     * Performs SASL authentication of the specified user. If SASL authentication was successful
     * then resource binding and session establishment will be performed. This method will return
     * the full JID provided by the server while binding a resource to the connection.<p>
     *
     * The server may assign a full JID with a username or resource different than the requested
     * by this method.
     *
     * @param username the username that is authenticating with the server.
     * @param password the password to send to the server.
     * @param resource the desired resource.
     * @throws XMPPErrorException 
     * @throws SASLErrorException 
     * @throws IOException 
     * @throws SmackException 
     * @throws InterruptedException 
     */
    public void authenticate(String username, String password, String resource)
                    throws XMPPErrorException, SASLErrorException, IOException,
                    SmackException, InterruptedException {
        SASLMechanism selectedMechanism = selectMechanism();
        if (selectedMechanism != null) {
            currentMechanism = selectedMechanism;

            synchronized (this) {
                currentMechanism.authenticate(username, connection.getHost(),
                                connection.getServiceName(), password);
                // Wait until SASL negotiation finishes
                wait(connection.getPacketReplyTimeout());
            }

            maybeThrowException();

            if (!authenticationSuccessful) {
                throw NoResponseException.newWith(connection);
            }
        }
        else {
            throw new SmackException(
                            "SASL Authentication failed. No known authentication mechanisims.");
        }
    }

    /**
     * Performs ANONYMOUS SASL authentication. If SASL authentication was successful
     * then resource binding and session establishment will be performed. This method will return
     * the full JID provided by the server while binding a resource to the connection.<p>
     *
     * The server will assign a full JID with a randomly generated resource and possibly with
     * no username.
     *
     * @throws SASLErrorException 
     * @throws XMPPErrorException if an error occures while authenticating.
     * @throws SmackException if there was no response from the server.
     * @throws InterruptedException 
     */
    public void authenticateAnonymously() throws SASLErrorException,
                    SmackException, XMPPErrorException, InterruptedException {
        currentMechanism = (new SASLAnonymous()).instanceForAuthentication(connection);

        // Wait until SASL negotiation finishes
        synchronized (this) {
            currentMechanism.authenticate(null, null, null, "");
            wait(connection.getPacketReplyTimeout());
        }

        maybeThrowException();

        if (!authenticationSuccessful) {
            throw NoResponseException.newWith(connection);
        }
    }

    private void maybeThrowException() throws SmackException, SASLErrorException {
        if (saslException != null){
            if (saslException instanceof SmackException) {
                throw (SmackException) saslException;
            } else if (saslException instanceof SASLErrorException) {
                throw (SASLErrorException) saslException;
            } else {
                throw new IllegalStateException("Unexpected exception type" , saslException);
            }
        }
    }

    /**
     * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
     * to <code>false</code>.
     * 
     * @param challenge a base64 encoded string representing the challenge.
     * @throws SmackException
     * @throws InterruptedException 
     */
    public void challengeReceived(String challenge) throws SmackException, InterruptedException {
        challengeReceived(challenge, false);
    }

    /**
     * The server is challenging the SASL authentication we just sent. Forward the challenge
     * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to
     * the server. The length of the challenge-response sequence varies according to the
     * SASLMechanism in use.
     *
     * @param challenge a base64 encoded string representing the challenge.
     * @param finalChallenge true if this is the last challenge send by the server within the success stanza
     * @throws SmackException
     * @throws InterruptedException
     */
    public void challengeReceived(String challenge, boolean finalChallenge) throws SmackException, InterruptedException {
        try {
            currentMechanism.challengeReceived(challenge, finalChallenge);
        } catch (InterruptedException | SmackException e) {
            authenticationFailed(e);
            throw e;
        }
    }

    /**
     * Notification message saying that SASL authentication was successful. The next step
     * would be to bind the resource.
     * @throws SmackException 
     * @throws InterruptedException 
     */
    public void authenticated(Success success) throws SmackException, InterruptedException {
        // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP
        // "receiving entity") can include "additional data with success" if appropriate for the
        // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML
        // character data of the <success/> element." The used SASL mechanism should be able to
        // verify the data send by the server in the success stanza, if any.
        if (success.getData() != null) {
            challengeReceived(success.getData(), true);
        }
        currentMechanism.checkIfSuccessfulOrThrow();
        authenticationSuccessful = true;
        // Wake up the thread that is waiting in the #authenticate method
        synchronized (this) {
            notify();
        }
    }

    /**
     * Notification message saying that SASL authentication has failed. The server may have
     * closed the connection depending on the number of possible retries.
     * 
     * @param saslFailure the SASL failure as reported by the server
     * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
     */
    public void authenticationFailed(SASLFailure saslFailure) {
        authenticationFailed(new SASLErrorException(currentMechanism.getName(), saslFailure));
    }

    public void authenticationFailed(Exception exception) {
        saslException = exception;
        // Wake up the thread that is waiting in the #authenticate method
        synchronized (this) {
            notify();
        }
    }

    public boolean authenticationSuccessful() {
        return authenticationSuccessful;
    }

    /**
     * Initializes the internal state in order to be able to be reused. The authentication
     * is used by the connection at the first login and then reused after the connection
     * is disconnected and then reconnected.
     */
    protected void init() {
        authenticationSuccessful = false;
        saslException = null;
    }

    private SASLMechanism selectMechanism() {
        // Locate the SASLMechanism to use
        SASLMechanism selectedMechanism = null;
        Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
        // Iterate in SASL Priority order over registered mechanisms
        while (it.hasNext()) {
            SASLMechanism mechanism = it.next();
            String mechanismName = mechanism.getName();
            synchronized (BLACKLISTED_MECHANISMS) {
                if (BLACKLISTED_MECHANISMS.contains(mechanismName)) {
                    continue;
                }
            }
            if (serverMechanisms().contains(mechanismName)) {
                // Create a new instance of the SASLMechanism for every authentication attempt.
                selectedMechanism = mechanism.instanceForAuthentication(connection);
                break;
            }
        }
        return selectedMechanism;
    }

    private List<String> serverMechanisms() {
        Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE);
        if (mechanisms == null) {
            LOGGER.warning("Server did not report any SASL mechanisms");
            return Collections.emptyList();
        }
        return mechanisms.getMechanisms();
    }
}