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 org.jivesoftware.smack.SmackException.NoResponseException;
  19. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  20. import org.jivesoftware.smack.packet.Mechanisms;
  21. import org.jivesoftware.smack.sasl.SASLAnonymous;
  22. import org.jivesoftware.smack.sasl.SASLErrorException;
  23. import org.jivesoftware.smack.sasl.SASLMechanism;
  24. import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
  25. import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;

  26. import javax.security.auth.callback.CallbackHandler;

  27. import java.io.IOException;
  28. import java.util.ArrayList;
  29. import java.util.Collections;
  30. import java.util.HashMap;
  31. import java.util.HashSet;
  32. import java.util.Iterator;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Set;
  36. import java.util.logging.Logger;

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

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

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

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

  58.     /**
  59.      * Registers a new SASL mechanism
  60.      *
  61.      * @param mechanism a SASLMechanism subclass.
  62.      */
  63.     public static void registerSASLMechanism(SASLMechanism mechanism) {
  64.         synchronized (REGISTERED_MECHANISMS) {
  65.             REGISTERED_MECHANISMS.add(mechanism);
  66.             Collections.sort(REGISTERED_MECHANISMS);
  67.         }
  68.     }

  69.     /**
  70.      * Returns the registered SASLMechanism sorted by the level of preference.
  71.      *
  72.      * @return the registered SASLMechanism sorted by the level of preference.
  73.      */
  74.     public static Map<String, String> getRegisterdSASLMechanisms() {
  75.         Map<String, String> answer = new HashMap<String, String>();
  76.         synchronized (REGISTERED_MECHANISMS) {
  77.             for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
  78.                 answer.put(mechanism.getClass().getName(), mechanism.getName());
  79.             }
  80.         }
  81.         return answer;
  82.     }

  83.     /**
  84.      * Unregister a SASLMechanism by it's full class name. For example
  85.      * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
  86.      *
  87.      * @param clazz the SASLMechanism class's name
  88.      * @return true if the given SASLMechanism was removed, false otherwise
  89.      */
  90.     public static boolean unregisterSASLMechanism(String clazz) {
  91.         synchronized (REGISTERED_MECHANISMS) {
  92.             Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
  93.             while (it.hasNext()) {
  94.                 SASLMechanism mechanism = it.next();
  95.                 if (mechanism.getClass().getName().equals(clazz)) {
  96.                     it.remove();
  97.                     return true;
  98.                 }
  99.             }
  100.         }
  101.         return false;
  102.     }

  103.     public static boolean blacklistSASLMechanism(String mechansim) {
  104.         synchronized(BLACKLISTED_MECHANISMS) {
  105.             return BLACKLISTED_MECHANISMS.add(mechansim);
  106.         }
  107.     }

  108.     public static boolean unBlacklistSASLMechanism(String mechanism) {
  109.         synchronized(BLACKLISTED_MECHANISMS) {
  110.             return BLACKLISTED_MECHANISMS.remove(mechanism);
  111.         }
  112.     }

  113.     public static Set<String> getBlacklistedSASLMechanisms() {
  114.         synchronized(BLACKLISTED_MECHANISMS) {
  115.             return new HashSet<String>(BLACKLISTED_MECHANISMS);
  116.         }
  117.     }

  118.     private final AbstractXMPPConnection connection;
  119.     private SASLMechanism currentMechanism = null;

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

  124.     /**
  125.      * Either of type {@link SmackException} or {@link SASLErrorException}
  126.      */
  127.     private Exception saslException;

  128.     SASLAuthentication(AbstractXMPPConnection connection) {
  129.         this.connection = connection;
  130.         this.init();
  131.     }

  132.     /**
  133.      * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
  134.      *
  135.      * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
  136.      */
  137.     public boolean hasAnonymousAuthentication() {
  138.         return serverMechanisms().contains("ANONYMOUS");
  139.     }

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

  148.     /**
  149.      * Performs SASL authentication of the specified user. If SASL authentication was successful
  150.      * then resource binding and session establishment will be performed. This method will return
  151.      * the full JID provided by the server while binding a resource to the connection.<p>
  152.      *
  153.      * The server may assign a full JID with a username or resource different than the requested
  154.      * by this method.
  155.      *
  156.      * @param resource the desired resource.
  157.      * @param cbh the CallbackHandler used to get information from the user
  158.      * @throws IOException
  159.      * @throws XMPPErrorException
  160.      * @throws SASLErrorException
  161.      * @throws SmackException
  162.      * @throws InterruptedException
  163.      */
  164.     public void authenticate(String resource, CallbackHandler cbh) throws IOException,
  165.                     XMPPErrorException, SASLErrorException, SmackException, InterruptedException {
  166.         SASLMechanism selectedMechanism = selectMechanism();
  167.         if (selectedMechanism != null) {
  168.             currentMechanism = selectedMechanism;
  169.             synchronized (this) {
  170.                 currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh);
  171.                 // Wait until SASL negotiation finishes
  172.                 wait(connection.getPacketReplyTimeout());
  173.             }

  174.             maybeThrowException();

  175.             if (!authenticationSuccessful) {
  176.                 throw NoResponseException.newWith(connection);
  177.             }
  178.         }
  179.         else {
  180.             throw new SmackException(
  181.                             "SASL Authentication failed. No known authentication mechanisims.");
  182.         }
  183.     }

  184.     /**
  185.      * Performs SASL authentication of the specified user. If SASL authentication was successful
  186.      * then resource binding and session establishment will be performed. This method will return
  187.      * the full JID provided by the server while binding a resource to the connection.<p>
  188.      *
  189.      * The server may assign a full JID with a username or resource different than the requested
  190.      * by this method.
  191.      *
  192.      * @param username the username that is authenticating with the server.
  193.      * @param password the password to send to the server.
  194.      * @param resource the desired resource.
  195.      * @throws XMPPErrorException
  196.      * @throws SASLErrorException
  197.      * @throws IOException
  198.      * @throws SmackException
  199.      * @throws InterruptedException
  200.      */
  201.     public void authenticate(String username, String password, String resource)
  202.                     throws XMPPErrorException, SASLErrorException, IOException,
  203.                     SmackException, InterruptedException {
  204.         SASLMechanism selectedMechanism = selectMechanism();
  205.         if (selectedMechanism != null) {
  206.             currentMechanism = selectedMechanism;

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

  213.             maybeThrowException();

  214.             if (!authenticationSuccessful) {
  215.                 throw NoResponseException.newWith(connection);
  216.             }
  217.         }
  218.         else {
  219.             throw new SmackException(
  220.                             "SASL Authentication failed. No known authentication mechanisims.");
  221.         }
  222.     }

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

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

  244.         maybeThrowException();

  245.         if (!authenticationSuccessful) {
  246.             throw NoResponseException.newWith(connection);
  247.         }
  248.     }

  249.     private void maybeThrowException() throws SmackException, SASLErrorException {
  250.         if (saslException != null){
  251.             if (saslException instanceof SmackException) {
  252.                 throw (SmackException) saslException;
  253.             } else if (saslException instanceof SASLErrorException) {
  254.                 throw (SASLErrorException) saslException;
  255.             } else {
  256.                 throw new IllegalStateException("Unexpected exception type" , saslException);
  257.             }
  258.         }
  259.     }

  260.     /**
  261.      * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
  262.      * to <code>false</code>.
  263.      *
  264.      * @param challenge a base64 encoded string representing the challenge.
  265.      * @throws SmackException
  266.      * @throws InterruptedException
  267.      */
  268.     public void challengeReceived(String challenge) throws SmackException, InterruptedException {
  269.         challengeReceived(challenge, false);
  270.     }

  271.     /**
  272.      * The server is challenging the SASL authentication we just sent. Forward the challenge
  273.      * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to
  274.      * the server. The length of the challenge-response sequence varies according to the
  275.      * SASLMechanism in use.
  276.      *
  277.      * @param challenge a base64 encoded string representing the challenge.
  278.      * @param finalChallenge true if this is the last challenge send by the server within the success stanza
  279.      * @throws SmackException
  280.      * @throws InterruptedException
  281.      */
  282.     public void challengeReceived(String challenge, boolean finalChallenge) throws SmackException, InterruptedException {
  283.         try {
  284.             currentMechanism.challengeReceived(challenge, finalChallenge);
  285.         } catch (InterruptedException | SmackException e) {
  286.             authenticationFailed(e);
  287.             throw e;
  288.         }
  289.     }

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

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

  322.     public void authenticationFailed(Exception exception) {
  323.         saslException = exception;
  324.         // Wake up the thread that is waiting in the #authenticate method
  325.         synchronized (this) {
  326.             notify();
  327.         }
  328.     }

  329.     public boolean authenticationSuccessful() {
  330.         return authenticationSuccessful;
  331.     }

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

  341.     private SASLMechanism selectMechanism() {
  342.         // Locate the SASLMechanism to use
  343.         SASLMechanism selectedMechanism = null;
  344.         Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
  345.         // Iterate in SASL Priority order over registered mechanisms
  346.         while (it.hasNext()) {
  347.             SASLMechanism mechanism = it.next();
  348.             String mechanismName = mechanism.getName();
  349.             synchronized (BLACKLISTED_MECHANISMS) {
  350.                 if (BLACKLISTED_MECHANISMS.contains(mechanismName)) {
  351.                     continue;
  352.                 }
  353.             }
  354.             if (serverMechanisms().contains(mechanismName)) {
  355.                 // Create a new instance of the SASLMechanism for every authentication attempt.
  356.                 selectedMechanism = mechanism.instanceForAuthentication(connection);
  357.                 break;
  358.             }
  359.         }
  360.         return selectedMechanism;
  361.     }

  362.     private List<String> serverMechanisms() {
  363.         Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE);
  364.         if (mechanisms == null) {
  365.             LOGGER.warning("Server did not report any SASL mechanisms");
  366.             return Collections.emptyList();
  367.         }
  368.         return mechanisms.getMechanisms();
  369.     }
  370. }