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();
}
}