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