SASLAuthentication.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smack;

  18. import java.io.IOException;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.HashSet;
  22. import java.util.Iterator;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.logging.Logger;

  28. import javax.net.ssl.SSLSession;
  29. import javax.security.auth.callback.CallbackHandler;

  30. import org.jivesoftware.smack.SmackException.NoResponseException;
  31. import org.jivesoftware.smack.SmackException.NotConnectedException;
  32. import org.jivesoftware.smack.SmackException.SmackSaslException;
  33. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  34. import org.jivesoftware.smack.packet.Mechanisms;
  35. import org.jivesoftware.smack.sasl.SASLErrorException;
  36. import org.jivesoftware.smack.sasl.SASLMechanism;
  37. import org.jivesoftware.smack.sasl.core.ScramSha1PlusMechanism;
  38. import org.jivesoftware.smack.sasl.packet.SaslNonza;
  39. import org.jivesoftware.smack.sasl.packet.SaslNonza.SASLFailure;
  40. import org.jivesoftware.smack.sasl.packet.SaslNonza.Success;
  41. import org.jivesoftware.smack.util.StringUtils;

  42. import org.jxmpp.jid.DomainBareJid;
  43. import org.jxmpp.jid.EntityBareJid;

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

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

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

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

  65.     static {
  66.         // Blacklist SCRAM-SHA-1-PLUS for now.
  67.         blacklistSASLMechanism(ScramSha1PlusMechanism.NAME);
  68.     }

  69.     /**
  70.      * Registers a new SASL mechanism.
  71.      *
  72.      * @param mechanism a SASLMechanism subclass.
  73.      */
  74.     public static void registerSASLMechanism(SASLMechanism mechanism) {
  75.         synchronized (REGISTERED_MECHANISMS) {
  76.             REGISTERED_MECHANISMS.add(mechanism);
  77.             Collections.sort(REGISTERED_MECHANISMS);
  78.         }
  79.     }

  80.     /**
  81.      * Returns the registered SASLMechanism sorted by the level of preference.
  82.      *
  83.      * @return the registered SASLMechanism sorted by the level of preference.
  84.      */
  85.     public static Map<String, String> getRegisterdSASLMechanisms() {
  86.         Map<String, String> answer = new LinkedHashMap<>();
  87.         synchronized (REGISTERED_MECHANISMS) {
  88.             for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
  89.                 answer.put(mechanism.getClass().getName(), mechanism.toString());
  90.             }
  91.         }
  92.         return answer;
  93.     }

  94.     public static boolean isSaslMechanismRegistered(String saslMechanism) {
  95.         synchronized (REGISTERED_MECHANISMS) {
  96.             for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
  97.                 if (mechanism.getName().equals(saslMechanism)) {
  98.                     return true;
  99.                 }
  100.             }
  101.         }
  102.         return false;
  103.     }

  104.     /**
  105.      * Unregister a SASLMechanism by it's full class name. For example
  106.      * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
  107.      *
  108.      * @param clazz the SASLMechanism class's name
  109.      * @return true if the given SASLMechanism was removed, false otherwise
  110.      */
  111.     public static boolean unregisterSASLMechanism(String clazz) {
  112.         synchronized (REGISTERED_MECHANISMS) {
  113.             Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
  114.             while (it.hasNext()) {
  115.                 SASLMechanism mechanism = it.next();
  116.                 if (mechanism.getClass().getName().equals(clazz)) {
  117.                     it.remove();
  118.                     return true;
  119.                 }
  120.             }
  121.         }
  122.         return false;
  123.     }

  124.     public static boolean blacklistSASLMechanism(String mechanism) {
  125.         synchronized (BLACKLISTED_MECHANISMS) {
  126.             return BLACKLISTED_MECHANISMS.add(mechanism);
  127.         }
  128.     }

  129.     public static boolean unBlacklistSASLMechanism(String mechanism) {
  130.         synchronized (BLACKLISTED_MECHANISMS) {
  131.             return BLACKLISTED_MECHANISMS.remove(mechanism);
  132.         }
  133.     }

  134.     public static Set<String> getBlacklistedSASLMechanisms() {
  135.         return Collections.unmodifiableSet(BLACKLISTED_MECHANISMS);
  136.     }

  137.     private final AbstractXMPPConnection connection;
  138.     private final ConnectionConfiguration configuration;
  139.     private SASLMechanism currentMechanism = null;

  140.     SASLAuthentication(AbstractXMPPConnection connection, ConnectionConfiguration configuration) {
  141.         this.configuration = configuration;
  142.         this.connection = connection;
  143.     }

  144.     /**
  145.      * Performs SASL authentication of the specified user. If SASL authentication was successful
  146.      * then resource binding and session establishment will be performed. This method will return
  147.      * the full JID provided by the server while binding a resource to the connection.<p>
  148.      *
  149.      * The server may assign a full JID with a username or resource different than the requested
  150.      * by this method.
  151.      *
  152.      * @param username the username that is authenticating with the server.
  153.      * @param password the password to send to the server.
  154.      * @param authzid the authorization identifier (typically null).
  155.      * @param sslSession the optional SSL/TLS session (if one was established)
  156.      * @return the used SASLMechanism.
  157.      * @throws XMPPErrorException if there was an XMPP error returned.
  158.      * @throws SASLErrorException if a SASL protocol error was returned.
  159.      * @throws IOException if an I/O error occurred.
  160.      * @throws InterruptedException if the calling thread was interrupted.
  161.      * @throws SmackSaslException if a SASL specific error occurred.
  162.      * @throws NotConnectedException if the XMPP connection is not connected.
  163.      * @throws NoResponseException if there was no response from the remote entity.
  164.      */
  165.     SASLMechanism authenticate(String username, String password, EntityBareJid authzid, SSLSession sslSession)
  166.                     throws XMPPErrorException, SASLErrorException, IOException,
  167.                     InterruptedException, SmackSaslException, NotConnectedException, NoResponseException {
  168.         final SASLMechanism mechanism = selectMechanism(authzid, password);
  169.         final CallbackHandler callbackHandler = configuration.getCallbackHandler();
  170.         final String host = connection.getHost();
  171.         final DomainBareJid xmppServiceDomain = connection.getXMPPServiceDomain();

  172.         synchronized (this) {
  173.             currentMechanism = mechanism;

  174.             if (callbackHandler != null) {
  175.                 currentMechanism.authenticate(host, xmppServiceDomain, callbackHandler, authzid, sslSession);
  176.             }
  177.             else {
  178.                 currentMechanism.authenticate(username, host, xmppServiceDomain, password, authzid, sslSession);
  179.             }

  180.             final long deadline = System.currentTimeMillis() + connection.getReplyTimeout();
  181.             while (!mechanism.isFinished()) {
  182.                 final long now = System.currentTimeMillis();
  183.                 if (now >= deadline) break;
  184.                 // Wait until SASL negotiation finishes
  185.                 wait(deadline - now);
  186.             }
  187.         }

  188.         mechanism.throwExceptionIfRequired();

  189.         return mechanism;
  190.     }

  191.     /**
  192.      * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
  193.      * to <code>false</code>.
  194.      *
  195.      * @param challenge the challenge Nonza.
  196.      * @throws SmackException if Smack detected an exceptional situation.
  197.      * @throws InterruptedException if the calling thread was interrupted.
  198.      */
  199.     void challengeReceived(SaslNonza.Challenge challenge) throws SmackException, InterruptedException {
  200.         challengeReceived(challenge.getData(), false);
  201.     }

  202.     /**
  203.      * The server is challenging the SASL authentication we just sent. Forward the challenge
  204.      * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to
  205.      * the server. The length of the challenge-response sequence varies according to the
  206.      * SASLMechanism in use.
  207.      *
  208.      * @param challenge a base64 encoded string representing the challenge.
  209.      * @param finalChallenge true if this is the last challenge send by the server within the success stanza
  210.      * @throws SmackSaslException if a SASL specific error occurred.
  211.      * @throws NotConnectedException if the XMPP connection is not connected.
  212.      * @throws InterruptedException if the calling thread was interrupted.
  213.      */
  214.     private void challengeReceived(String challenge, boolean finalChallenge) throws SmackSaslException, NotConnectedException, InterruptedException {
  215.         SASLMechanism mechanism;
  216.         synchronized (this) {
  217.             mechanism = currentMechanism;
  218.         }
  219.         mechanism.challengeReceived(challenge, finalChallenge);
  220.     }

  221.     /**
  222.      * Notification message saying that SASL authentication was successful. The next step
  223.      * would be to bind the resource.
  224.      * @param success result of the authentication.
  225.      * @throws InterruptedException if the calling thread was interrupted.
  226.      * @throws NotConnectedException if the XMPP connection is not connected.
  227.      * @throws SmackSaslException if a SASL specific error occurred.
  228.      */
  229.     void authenticated(Success success) throws InterruptedException, SmackSaslException, NotConnectedException {
  230.         // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP
  231.         // "receiving entity") can include "additional data with success" if appropriate for the
  232.         // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML
  233.         // character data of the <success/> element." The used SASL mechanism should be able to
  234.         // verify the data send by the server in the success stanza, if any.
  235.         if (success.getData() != null) {
  236.             challengeReceived(success.getData(), true);
  237.         }

  238.         // Wake up the thread that is waiting in the #authenticate method
  239.         synchronized (this) {
  240.             currentMechanism.afterFinalSaslChallenge();

  241.             notify();
  242.         }
  243.     }

  244.     /**
  245.      * Notification message saying that SASL authentication has failed. The server may have
  246.      * closed the connection depending on the number of possible retries.
  247.      *
  248.      * @param saslFailure the SASL failure as reported by the server
  249.      * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
  250.      */
  251.     void authenticationFailed(SASLFailure saslFailure) {
  252.         SASLErrorException saslErrorException;
  253.         synchronized (this) {
  254.             saslErrorException = new SASLErrorException(currentMechanism.getName(), saslFailure);
  255.         }
  256.         authenticationFailed(saslErrorException);
  257.     }

  258.     void authenticationFailed(Exception exception) {
  259.         // Wake up the thread that is waiting in the #authenticate method
  260.         synchronized (this) {
  261.             currentMechanism.setException(exception);
  262.             notify();
  263.         }
  264.     }

  265.     public boolean authenticationSuccessful() {
  266.         synchronized (this) {
  267.             if (currentMechanism == null) {
  268.                 return false;
  269.             }
  270.             return currentMechanism.isAuthenticationSuccessful();
  271.         }
  272.     }

  273.     String getNameOfLastUsedSaslMechansism() {
  274.         SASLMechanism lastUsedMech = currentMechanism;
  275.         if (lastUsedMech == null) {
  276.             return null;
  277.         }
  278.         return lastUsedMech.getName();
  279.     }

  280.     private SASLMechanism selectMechanism(EntityBareJid authzid, String password) throws SmackException.SmackSaslException {
  281.         final boolean passwordAvailable = StringUtils.isNotEmpty(password);

  282.         Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
  283.         final List<String> serverMechanisms = getServerMechanisms();
  284.         if (serverMechanisms.isEmpty()) {
  285.             LOGGER.warning("Server did not report any SASL mechanisms");
  286.         }

  287.         List<String> skipReasons = new ArrayList<>();

  288.         // Iterate in SASL Priority order over registered mechanisms
  289.         while (it.hasNext()) {
  290.             SASLMechanism mechanism = it.next();
  291.             String mechanismName = mechanism.getName();

  292.             if (!serverMechanisms.contains(mechanismName)) {
  293.                 continue;
  294.             }

  295.             synchronized (BLACKLISTED_MECHANISMS) {
  296.                 if (BLACKLISTED_MECHANISMS.contains(mechanismName)) {
  297.                     continue;
  298.                 }
  299.             }

  300.             if (!configuration.isEnabledSaslMechanism(mechanismName)) {
  301.                 continue;
  302.             }

  303.             if (authzid != null && !mechanism.authzidSupported()) {
  304.                 skipReasons.add("Skipping " + mechanism + " because authzid is required by not supported by this SASL mechanism");
  305.                 continue;
  306.             }

  307.             if (mechanism.requiresPassword() && !passwordAvailable) {
  308.                 skipReasons.add("Skipping " + mechanism + " because a password is required for it, but none was provided to the connection configuration");
  309.                 continue;
  310.             }

  311.             // Create a new instance of the SASLMechanism for every authentication attempt.
  312.             return mechanism.instanceForAuthentication(connection, configuration);
  313.         }

  314.         synchronized (BLACKLISTED_MECHANISMS) {
  315.             // @formatter:off
  316.             throw new SmackException.SmackSaslException(
  317.                             "No supported and enabled SASL Mechanism provided by server. " +
  318.                             "Server announced mechanisms: " + serverMechanisms + ". " +
  319.                             "Registered SASL mechanisms with Smack: " + REGISTERED_MECHANISMS + ". " +
  320.                             "Enabled SASL mechanisms for this connection: " + configuration.getEnabledSaslMechanisms() + ". " +
  321.                             "Blacklisted SASL mechanisms: " + BLACKLISTED_MECHANISMS + ". " +
  322.                             "Skip reasons: " + skipReasons
  323.                             );
  324.             // @formatter;on
  325.         }
  326.     }

  327.     private List<String> getServerMechanisms() {
  328.         Mechanisms mechanisms = connection.getFeature(Mechanisms.class);
  329.         if (mechanisms == null) {
  330.             return Collections.emptyList();
  331.         }
  332.         return mechanisms.getMechanisms();
  333.     }
  334. }