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