001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smack;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.logging.Logger;
030
031import javax.net.ssl.SSLSession;
032import javax.security.auth.callback.CallbackHandler;
033
034import org.jivesoftware.smack.SmackException.NoResponseException;
035import org.jivesoftware.smack.SmackException.NotConnectedException;
036import org.jivesoftware.smack.SmackException.SmackSaslException;
037import org.jivesoftware.smack.XMPPException.XMPPErrorException;
038import org.jivesoftware.smack.packet.Mechanisms;
039import org.jivesoftware.smack.sasl.SASLErrorException;
040import org.jivesoftware.smack.sasl.SASLMechanism;
041import org.jivesoftware.smack.sasl.core.ScramSha1PlusMechanism;
042import org.jivesoftware.smack.sasl.packet.SaslNonza;
043import org.jivesoftware.smack.sasl.packet.SaslNonza.SASLFailure;
044import org.jivesoftware.smack.sasl.packet.SaslNonza.Success;
045
046import org.jxmpp.jid.DomainBareJid;
047import org.jxmpp.jid.EntityBareJid;
048
049/**
050 * <p>This class is responsible authenticating the user using SASL, binding the resource
051 * to the connection and establishing a session with the server.</p>
052 *
053 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
054 * register with the server or authenticate using SASL. If the
055 * server supports SASL then Smack will try to authenticate using SASL..</p>
056 *
057 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
058 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
059 * {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms.
060 *
061 * @see org.jivesoftware.smack.sasl.SASLMechanism
062 *
063 * @author Gaston Dombiak
064 * @author Jay Kline
065 */
066public final class SASLAuthentication {
067
068    private static final Logger LOGGER = Logger.getLogger(SASLAuthentication.class.getName());
069
070    private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<>();
071
072    private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<>();
073
074    static {
075        // Blacklist SCRAM-SHA-1-PLUS for now.
076        blacklistSASLMechanism(ScramSha1PlusMechanism.NAME);
077    }
078
079    /**
080     * Registers a new SASL mechanism.
081     *
082     * @param mechanism a SASLMechanism subclass.
083     */
084    public static void registerSASLMechanism(SASLMechanism mechanism) {
085        synchronized (REGISTERED_MECHANISMS) {
086            REGISTERED_MECHANISMS.add(mechanism);
087            Collections.sort(REGISTERED_MECHANISMS);
088        }
089    }
090
091    /**
092     * Returns the registered SASLMechanism sorted by the level of preference.
093     *
094     * @return the registered SASLMechanism sorted by the level of preference.
095     */
096    public static Map<String, String> getRegisterdSASLMechanisms() {
097        Map<String, String> answer = new LinkedHashMap<>();
098        synchronized (REGISTERED_MECHANISMS) {
099            for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
100                answer.put(mechanism.getClass().getName(), mechanism.toString());
101            }
102        }
103        return answer;
104    }
105
106    public static boolean isSaslMechanismRegistered(String saslMechanism) {
107        synchronized (REGISTERED_MECHANISMS) {
108            for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
109                if (mechanism.getName().equals(saslMechanism)) {
110                    return true;
111                }
112            }
113        }
114        return false;
115    }
116
117    /**
118     * Unregister a SASLMechanism by it's full class name. For example
119     * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
120     *
121     * @param clazz the SASLMechanism class's name
122     * @return true if the given SASLMechanism was removed, false otherwise
123     */
124    public static boolean unregisterSASLMechanism(String clazz) {
125        synchronized (REGISTERED_MECHANISMS) {
126            Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
127            while (it.hasNext()) {
128                SASLMechanism mechanism = it.next();
129                if (mechanism.getClass().getName().equals(clazz)) {
130                    it.remove();
131                    return true;
132                }
133            }
134        }
135        return false;
136    }
137
138    public static boolean blacklistSASLMechanism(String mechanism) {
139        synchronized (BLACKLISTED_MECHANISMS) {
140            return BLACKLISTED_MECHANISMS.add(mechanism);
141        }
142    }
143
144    public static boolean unBlacklistSASLMechanism(String mechanism) {
145        synchronized (BLACKLISTED_MECHANISMS) {
146            return BLACKLISTED_MECHANISMS.remove(mechanism);
147        }
148    }
149
150    public static Set<String> getBlacklistedSASLMechanisms() {
151        return Collections.unmodifiableSet(BLACKLISTED_MECHANISMS);
152    }
153
154    private final AbstractXMPPConnection connection;
155    private final ConnectionConfiguration configuration;
156    private SASLMechanism currentMechanism = null;
157
158    SASLAuthentication(AbstractXMPPConnection connection, ConnectionConfiguration configuration) {
159        this.configuration = configuration;
160        this.connection = connection;
161    }
162
163    /**
164     * Performs SASL authentication of the specified user. If SASL authentication was successful
165     * then resource binding and session establishment will be performed. This method will return
166     * the full JID provided by the server while binding a resource to the connection.<p>
167     *
168     * The server may assign a full JID with a username or resource different than the requested
169     * by this method.
170     *
171     * @param username the username that is authenticating with the server.
172     * @param password the password to send to the server.
173     * @param authzid the authorization identifier (typically null).
174     * @param sslSession the optional SSL/TLS session (if one was established)
175     * @return the used SASLMechanism.
176     * @throws XMPPErrorException if there was an XMPP error returned.
177     * @throws SASLErrorException if a SASL protocol error was returned.
178     * @throws IOException if an I/O error occurred.
179     * @throws InterruptedException if the calling thread was interrupted.
180     * @throws SmackSaslException if a SASL specific error occurred.
181     * @throws NotConnectedException if the XMPP connection is not connected.
182     * @throws NoResponseException if there was no response from the remote entity.
183     */
184    SASLMechanism authenticate(String username, String password, EntityBareJid authzid, SSLSession sslSession)
185                    throws XMPPErrorException, SASLErrorException, IOException,
186                    InterruptedException, SmackSaslException, NotConnectedException, NoResponseException {
187        final SASLMechanism mechanism = selectMechanism(authzid);
188        final CallbackHandler callbackHandler = configuration.getCallbackHandler();
189        final String host = connection.getHost();
190        final DomainBareJid xmppServiceDomain = connection.getXMPPServiceDomain();
191
192        synchronized (this) {
193            currentMechanism = mechanism;
194
195            if (callbackHandler != null) {
196                currentMechanism.authenticate(host, xmppServiceDomain, callbackHandler, authzid, sslSession);
197            }
198            else {
199                currentMechanism.authenticate(username, host, xmppServiceDomain, password, authzid, sslSession);
200            }
201
202            final long deadline = System.currentTimeMillis() + connection.getReplyTimeout();
203            while (!mechanism.isFinished()) {
204                final long now = System.currentTimeMillis();
205                if (now >= deadline) break;
206                // Wait until SASL negotiation finishes
207                wait(deadline - now);
208            }
209        }
210
211        mechanism.throwExceptionIfRequired();
212
213        return mechanism;
214    }
215
216    /**
217     * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
218     * to <code>false</code>.
219     *
220     * @param challenge the challenge Nonza.
221     * @throws SmackException if Smack detected an exceptional situation.
222     * @throws InterruptedException if the calling thread was interrupted.
223     */
224    void challengeReceived(SaslNonza.Challenge challenge) throws SmackException, InterruptedException {
225        challengeReceived(challenge.getData(), false);
226    }
227
228    /**
229     * The server is challenging the SASL authentication we just sent. Forward the challenge
230     * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to
231     * the server. The length of the challenge-response sequence varies according to the
232     * SASLMechanism in use.
233     *
234     * @param challenge a base64 encoded string representing the challenge.
235     * @param finalChallenge true if this is the last challenge send by the server within the success stanza
236     * @throws SmackSaslException if a SASL specific error occurred.
237     * @throws NotConnectedException if the XMPP connection is not connected.
238     * @throws InterruptedException if the calling thread was interrupted.
239     */
240    private void challengeReceived(String challenge, boolean finalChallenge) throws SmackSaslException, NotConnectedException, InterruptedException {
241        SASLMechanism mechanism;
242        synchronized (this) {
243            mechanism = currentMechanism;
244        }
245        mechanism.challengeReceived(challenge, finalChallenge);
246    }
247
248    /**
249     * Notification message saying that SASL authentication was successful. The next step
250     * would be to bind the resource.
251     * @param success result of the authentication.
252     * @throws SmackException if Smack detected an exceptional situation.
253     * @throws InterruptedException if the calling thread was interrupted.
254     * @throws NotConnectedException if the XMPP connection is not connected.
255     * @throws SmackSaslException if a SASL specific error occurred.
256     */
257    void authenticated(Success success) throws InterruptedException, SmackSaslException, NotConnectedException {
258        // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP
259        // "receiving entity") can include "additional data with success" if appropriate for the
260        // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML
261        // character data of the <success/> element." The used SASL mechanism should be able to
262        // verify the data send by the server in the success stanza, if any.
263        if (success.getData() != null) {
264            challengeReceived(success.getData(), true);
265        }
266
267        // Wake up the thread that is waiting in the #authenticate method
268        synchronized (this) {
269            currentMechanism.afterFinalSaslChallenge();
270
271            notify();
272        }
273    }
274
275    /**
276     * Notification message saying that SASL authentication has failed. The server may have
277     * closed the connection depending on the number of possible retries.
278     *
279     * @param saslFailure the SASL failure as reported by the server
280     * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
281     */
282    void authenticationFailed(SASLFailure saslFailure) {
283        SASLErrorException saslErrorException;
284        synchronized (this) {
285            saslErrorException = new SASLErrorException(currentMechanism.getName(), saslFailure);
286        }
287        authenticationFailed(saslErrorException);
288    }
289
290    void authenticationFailed(Exception exception) {
291        // Wake up the thread that is waiting in the #authenticate method
292        synchronized (this) {
293            currentMechanism.setException(exception);
294            notify();
295        }
296    }
297
298    public boolean authenticationSuccessful() {
299        synchronized (this) {
300            if (currentMechanism == null) {
301                return false;
302            }
303            return currentMechanism.isAuthenticationSuccessful();
304        }
305    }
306
307    String getNameOfLastUsedSaslMechansism() {
308        SASLMechanism lastUsedMech = currentMechanism;
309        if (lastUsedMech == null) {
310            return null;
311        }
312        return lastUsedMech.getName();
313    }
314
315    private SASLMechanism selectMechanism(EntityBareJid authzid) throws SmackException.SmackSaslException {
316        Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
317        final List<String> serverMechanisms = getServerMechanisms();
318        if (serverMechanisms.isEmpty()) {
319            LOGGER.warning("Server did not report any SASL mechanisms");
320        }
321        // Iterate in SASL Priority order over registered mechanisms
322        while (it.hasNext()) {
323            SASLMechanism mechanism = it.next();
324            String mechanismName = mechanism.getName();
325            synchronized (BLACKLISTED_MECHANISMS) {
326                if (BLACKLISTED_MECHANISMS.contains(mechanismName)) {
327                    continue;
328                }
329            }
330            if (!configuration.isEnabledSaslMechanism(mechanismName)) {
331                continue;
332            }
333            if (authzid != null) {
334                if (!mechanism.authzidSupported()) {
335                    LOGGER.fine("Skipping " + mechanism + " because authzid is required by not supported by this SASL mechanism");
336                    continue;
337                }
338            }
339            if (serverMechanisms.contains(mechanismName)) {
340                // Create a new instance of the SASLMechanism for every authentication attempt.
341                return mechanism.instanceForAuthentication(connection, configuration);
342            }
343        }
344
345        synchronized (BLACKLISTED_MECHANISMS) {
346            // @formatter:off
347            throw new SmackException.SmackSaslException(
348                            "No supported and enabled SASL Mechanism provided by server. " +
349                            "Server announced mechanisms: " + serverMechanisms + ". " +
350                            "Registered SASL mechanisms with Smack: " + REGISTERED_MECHANISMS + ". " +
351                            "Enabled SASL mechanisms for this connection: " + configuration.getEnabledSaslMechanisms() + ". " +
352                            "Blacklisted SASL mechanisms: " + BLACKLISTED_MECHANISMS + '.'
353                            );
354            // @formatter;on
355        }
356    }
357
358    private List<String> getServerMechanisms() {
359        Mechanisms mechanisms = connection.getFeature(Mechanisms.class);
360        if (mechanisms == null) {
361            return Collections.emptyList();
362        }
363        return mechanisms.getMechanisms();
364    }
365}