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