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