1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import org.jivesoftware.smack.filter.PacketIDFilter;
24import org.jivesoftware.smack.packet.Bind;
25import org.jivesoftware.smack.packet.IQ;
26import org.jivesoftware.smack.packet.Packet;
27import org.jivesoftware.smack.packet.Session;
28import org.jivesoftware.smack.sasl.*;
29
30import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
31import java.io.IOException;
32import java.lang.reflect.Constructor;
33import java.util.*;
34
35/**
36 * <p>This class is responsible authenticating the user using SASL, binding the resource
37 * to the connection and establishing a session with the server.</p>
38 *
39 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
40 * register with the server, authenticate using Non-SASL or authenticate using SASL. If the
41 * server supports SASL then Smack will first try to authenticate using SASL. But if that
42 * fails then Non-SASL will be tried.</p>
43 *
44 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
45 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
46 * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
47 * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
48 * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
49 *
50 * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for
51 * the connection. If no resource is passed in {@link #authenticate(String, String, String)}
52 * then the server will assign a resource for the connection. In case a resource is passed
53 * then the server will receive the desired resource but may assign a modified resource for
54 * the connection.</p>
55 *
56 * <p>Once a resource has been binded and if the server supports sessions then Smack will establish
57 * a session so that instant messaging and presence functionalities may be used.</p>
58 *
59 * @see org.jivesoftware.smack.sasl.SASLMechanism
60 *
61 * @author Gaston Dombiak
62 * @author Jay Kline
63 */
64public class SASLAuthentication implements UserAuthentication {
65
66    private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
67    private static List<String> mechanismsPreferences = new ArrayList<String>();
68
69    private Connection connection;
70    private Collection<String> serverMechanisms = new ArrayList<String>();
71    private SASLMechanism currentMechanism = null;
72    /**
73     * Boolean indicating if SASL negotiation has finished and was successful.
74     */
75    private boolean saslNegotiated;
76    /**
77     * Boolean indication if SASL authentication has failed. When failed the server may end
78     * the connection.
79     */
80    private boolean saslFailed;
81    private boolean resourceBinded;
82    private boolean sessionSupported;
83    /**
84     * The SASL related error condition if there was one provided by the server.
85     */
86    private String errorCondition;
87
88    static {
89
90        // Register SASL mechanisms supported by Smack
91        registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
92        registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
93        registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
94        registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
95        registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
96        registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
97
98        supportSASLMechanism("GSSAPI",0);
99        supportSASLMechanism("DIGEST-MD5",1);
100        supportSASLMechanism("CRAM-MD5",2);
101        supportSASLMechanism("PLAIN",3);
102        supportSASLMechanism("ANONYMOUS",4);
103
104    }
105
106    /**
107     * Registers a new SASL mechanism
108     *
109     * @param name   common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
110     * @param mClass a SASLMechanism subclass.
111     */
112    public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {
113        implementedMechanisms.put(name, mClass);
114    }
115
116    /**
117     * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
118     * be possible to authenticate users using the removed SASL mechanism. It also removes the
119     * mechanism from the supported list.
120     *
121     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
122     */
123    public static void unregisterSASLMechanism(String name) {
124        implementedMechanisms.remove(name);
125        mechanismsPreferences.remove(name);
126    }
127
128
129    /**
130     * Registers a new SASL mechanism in the specified preference position. The client will try
131     * to authenticate using the most prefered SASL mechanism that is also supported by the server.
132     * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
133     *
134     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
135     */
136    public static void supportSASLMechanism(String name) {
137        mechanismsPreferences.add(0, name);
138    }
139
140    /**
141     * Registers a new SASL mechanism in the specified preference position. The client will try
142     * to authenticate using the most prefered SASL mechanism that is also supported by the server.
143     * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.
144     * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be
145     * registered via {@link #registerSASLMechanism(String, Class)}
146     *
147     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
148     * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
149     */
150    public static void supportSASLMechanism(String name, int index) {
151        mechanismsPreferences.add(index, name);
152    }
153
154    /**
155     * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
156     * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
157     * is still registered, but will just not be used.
158     *
159     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
160     */
161    public static void unsupportSASLMechanism(String name) {
162        mechanismsPreferences.remove(name);
163    }
164
165    /**
166     * Returns the registerd SASLMechanism classes sorted by the level of preference.
167     *
168     * @return the registerd SASLMechanism classes sorted by the level of preference.
169     */
170    public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
171        List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
172        for (String mechanismsPreference : mechanismsPreferences) {
173            answer.add(implementedMechanisms.get(mechanismsPreference));
174        }
175        return answer;
176    }
177
178    SASLAuthentication(Connection connection) {
179        super();
180        this.connection = connection;
181        this.init();
182    }
183
184    /**
185     * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
186     *
187     * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
188     */
189    public boolean hasAnonymousAuthentication() {
190        return serverMechanisms.contains("ANONYMOUS");
191    }
192
193    /**
194     * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
195     *
196     * @return true if the server offered SASL authentication besides ANONYMOUS SASL.
197     */
198    public boolean hasNonAnonymousAuthentication() {
199        return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
200    }
201
202    /**
203     * Performs SASL authentication of the specified user. If SASL authentication was successful
204     * then resource binding and session establishment will be performed. This method will return
205     * the full JID provided by the server while binding a resource to the connection.<p>
206     *
207     * The server may assign a full JID with a username or resource different than the requested
208     * by this method.
209     *
210     * @param username the username that is authenticating with the server.
211     * @param resource the desired resource.
212     * @param cbh the CallbackHandler used to get information from the user
213     * @return the full JID provided by the server while binding a resource to the connection.
214     * @throws XMPPException if an error occures while authenticating.
215     */
216    public String authenticate(String username, String resource, CallbackHandler cbh)
217            throws XMPPException {
218        // Locate the SASLMechanism to use
219        String selectedMechanism = null;
220        for (String mechanism : mechanismsPreferences) {
221            if (implementedMechanisms.containsKey(mechanism) &&
222                    serverMechanisms.contains(mechanism)) {
223                selectedMechanism = mechanism;
224                break;
225            }
226        }
227        if (selectedMechanism != null) {
228            // A SASL mechanism was found. Authenticate using the selected mechanism and then
229            // proceed to bind a resource
230            try {
231                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
232                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
233                currentMechanism = constructor.newInstance(this);
234                // Trigger SASL authentication with the selected mechanism. We use
235                // connection.getHost() since GSAPI requires the FQDN of the server, which
236                // may not match the XMPP domain.
237                currentMechanism.authenticate(username, connection.getHost(), cbh);
238
239                // Wait until SASL negotiation finishes
240                synchronized (this) {
241                    if (!saslNegotiated && !saslFailed) {
242                        try {
243                            wait(30000);
244                        }
245                        catch (InterruptedException e) {
246                            // Ignore
247                        }
248                    }
249                }
250
251                if (saslFailed) {
252                    // SASL authentication failed and the server may have closed the connection
253                    // so throw an exception
254                    if (errorCondition != null) {
255                        throw new XMPPException("SASL authentication " +
256                                selectedMechanism + " failed: " + errorCondition);
257                    }
258                    else {
259                        throw new XMPPException("SASL authentication failed using mechanism " +
260                                selectedMechanism);
261                    }
262                }
263
264                if (saslNegotiated) {
265                    // Bind a resource for this connection and
266                    return bindResourceAndEstablishSession(resource);
267                } else {
268                    // SASL authentication failed
269                }
270            }
271            catch (XMPPException e) {
272                throw e;
273            }
274            catch (Exception e) {
275                e.printStackTrace();
276            }
277        }
278        else {
279            throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");
280        }
281        throw new XMPPException("SASL authentication failed");
282    }
283
284    /**
285     * Performs SASL authentication of the specified user. If SASL authentication was successful
286     * then resource binding and session establishment will be performed. This method will return
287     * the full JID provided by the server while binding a resource to the connection.<p>
288     *
289     * The server may assign a full JID with a username or resource different than the requested
290     * by this method.
291     *
292     * @param username the username that is authenticating with the server.
293     * @param password the password to send to the server.
294     * @param resource the desired resource.
295     * @return the full JID provided by the server while binding a resource to the connection.
296     * @throws XMPPException if an error occures while authenticating.
297     */
298    public String authenticate(String username, String password, String resource)
299            throws XMPPException {
300        // Locate the SASLMechanism to use
301        String selectedMechanism = null;
302        for (String mechanism : mechanismsPreferences) {
303            if (implementedMechanisms.containsKey(mechanism) &&
304                    serverMechanisms.contains(mechanism)) {
305                selectedMechanism = mechanism;
306                break;
307            }
308        }
309        if (selectedMechanism != null) {
310            // A SASL mechanism was found. Authenticate using the selected mechanism and then
311            // proceed to bind a resource
312            try {
313                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
314                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
315                currentMechanism = constructor.newInstance(this);
316                // Trigger SASL authentication with the selected mechanism. We use
317                // connection.getHost() since GSAPI requires the FQDN of the server, which
318                // may not match the XMPP domain.
319                currentMechanism.authenticate(username, connection.getServiceName(), password);
320
321                // Wait until SASL negotiation finishes
322                synchronized (this) {
323                    if (!saslNegotiated && !saslFailed) {
324                        try {
325                            wait(30000);
326                        }
327                        catch (InterruptedException e) {
328                            // Ignore
329                        }
330                    }
331                }
332
333                if (saslFailed) {
334                    // SASL authentication failed and the server may have closed the connection
335                    // so throw an exception
336                    if (errorCondition != null) {
337                        throw new XMPPException("SASL authentication " +
338                                selectedMechanism + " failed: " + errorCondition);
339                    }
340                    else {
341                        throw new XMPPException("SASL authentication failed using mechanism " +
342                                selectedMechanism);
343                    }
344                }
345
346                if (saslNegotiated) {
347                    // Bind a resource for this connection and
348                    return bindResourceAndEstablishSession(resource);
349                }
350                else {
351                    // SASL authentication failed so try a Non-SASL authentication
352                    return new NonSASLAuthentication(connection)
353                            .authenticate(username, password, resource);
354                }
355            }
356            catch (XMPPException e) {
357                throw e;
358            }
359            catch (Exception e) {
360                e.printStackTrace();
361                // SASL authentication failed so try a Non-SASL authentication
362                return new NonSASLAuthentication(connection)
363                        .authenticate(username, password, resource);
364            }
365        }
366        else {
367            // No SASL method was found so try a Non-SASL authentication
368            return new NonSASLAuthentication(connection).authenticate(username, password, resource);
369        }
370    }
371
372    /**
373     * Performs ANONYMOUS SASL authentication. If SASL authentication was successful
374     * then resource binding and session establishment will be performed. This method will return
375     * the full JID provided by the server while binding a resource to the connection.<p>
376     *
377     * The server will assign a full JID with a randomly generated resource and possibly with
378     * no username.
379     *
380     * @return the full JID provided by the server while binding a resource to the connection.
381     * @throws XMPPException if an error occures while authenticating.
382     */
383    public String authenticateAnonymously() throws XMPPException {
384        try {
385            currentMechanism = new SASLAnonymous(this);
386            currentMechanism.authenticate(null,null,"");
387
388            // Wait until SASL negotiation finishes
389            synchronized (this) {
390                if (!saslNegotiated && !saslFailed) {
391                    try {
392                        wait(5000);
393                    }
394                    catch (InterruptedException e) {
395                        // Ignore
396                    }
397                }
398            }
399
400            if (saslFailed) {
401                // SASL authentication failed and the server may have closed the connection
402                // so throw an exception
403                if (errorCondition != null) {
404                    throw new XMPPException("SASL authentication failed: " + errorCondition);
405                }
406                else {
407                    throw new XMPPException("SASL authentication failed");
408                }
409            }
410
411            if (saslNegotiated) {
412                // Bind a resource for this connection and
413                return bindResourceAndEstablishSession(null);
414            }
415            else {
416                return new NonSASLAuthentication(connection).authenticateAnonymously();
417            }
418        } catch (IOException e) {
419            return new NonSASLAuthentication(connection).authenticateAnonymously();
420        }
421    }
422
423    private String bindResourceAndEstablishSession(String resource) throws XMPPException {
424        // Wait until server sends response containing the <bind> element
425        synchronized (this) {
426            if (!resourceBinded) {
427                try {
428                    wait(30000);
429                }
430                catch (InterruptedException e) {
431                    // Ignore
432                }
433            }
434        }
435
436        if (!resourceBinded) {
437            // Server never offered resource binding
438            throw new XMPPException("Resource binding not offered by server");
439        }
440
441        Bind bindResource = new Bind();
442        bindResource.setResource(resource);
443
444        PacketCollector collector = connection
445                .createPacketCollector(new PacketIDFilter(bindResource.getPacketID()));
446        // Send the packet
447        connection.sendPacket(bindResource);
448        // Wait up to a certain number of seconds for a response from the server.
449        Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
450        collector.cancel();
451        if (response == null) {
452            throw new XMPPException("No response from the server.");
453        }
454        // If the server replied with an error, throw an exception.
455        else if (response.getType() == IQ.Type.ERROR) {
456            throw new XMPPException(response.getError());
457        }
458        String userJID = response.getJid();
459
460        if (sessionSupported) {
461            Session session = new Session();
462            collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID()));
463            // Send the packet
464            connection.sendPacket(session);
465            // Wait up to a certain number of seconds for a response from the server.
466            IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
467            collector.cancel();
468            if (ack == null) {
469                throw new XMPPException("No response from the server.");
470            }
471            // If the server replied with an error, throw an exception.
472            else if (ack.getType() == IQ.Type.ERROR) {
473                throw new XMPPException(ack.getError());
474            }
475        }
476        return userJID;
477    }
478
479    /**
480     * Sets the available SASL mechanism reported by the server. The server will report the
481     * available SASL mechanism once the TLS negotiation was successful. This information is
482     * stored and will be used when doing the authentication for logging in the user.
483     *
484     * @param mechanisms collection of strings with the available SASL mechanism reported
485     *                   by the server.
486     */
487    void setAvailableSASLMethods(Collection<String> mechanisms) {
488        this.serverMechanisms = mechanisms;
489    }
490
491    /**
492     * Returns true if the user was able to authenticate with the server usins SASL.
493     *
494     * @return true if the user was able to authenticate with the server usins SASL.
495     */
496    public boolean isAuthenticated() {
497        return saslNegotiated;
498    }
499
500    /**
501     * The server is challenging the SASL authentication we just sent. Forward the challenge
502     * to the current SASLMechanism we are using. The SASLMechanism will send a response to
503     * the server. The length of the challenge-response sequence varies according to the
504     * SASLMechanism in use.
505     *
506     * @param challenge a base64 encoded string representing the challenge.
507     * @throws IOException If a network error occures while authenticating.
508     */
509    void challengeReceived(String challenge) throws IOException {
510        currentMechanism.challengeReceived(challenge);
511    }
512
513    /**
514     * Notification message saying that SASL authentication was successful. The next step
515     * would be to bind the resource.
516     */
517    void authenticated() {
518        synchronized (this) {
519            saslNegotiated = true;
520            // Wake up the thread that is waiting in the #authenticate method
521            notify();
522        }
523    }
524
525    /**
526     * Notification message saying that SASL authentication has failed. The server may have
527     * closed the connection depending on the number of possible retries.
528     *
529     * @deprecated replaced by {@see #authenticationFailed(String)}.
530     */
531    void authenticationFailed() {
532        authenticationFailed(null);
533    }
534
535    /**
536     * Notification message saying that SASL authentication has failed. The server may have
537     * closed the connection depending on the number of possible retries.
538     *
539     * @param condition the error condition provided by the server.
540     */
541    void authenticationFailed(String condition) {
542        synchronized (this) {
543            saslFailed = true;
544            errorCondition = condition;
545            // Wake up the thread that is waiting in the #authenticate method
546            notify();
547        }
548    }
549
550    /**
551     * Notification message saying that the server requires the client to bind a
552     * resource to the stream.
553     */
554    void bindingRequired() {
555        synchronized (this) {
556            resourceBinded = true;
557            // Wake up the thread that is waiting in the #authenticate method
558            notify();
559        }
560    }
561
562    public void send(Packet stanza) {
563        connection.sendPacket(stanza);
564    }
565
566    /**
567     * Notification message saying that the server supports sessions. When a server supports
568     * sessions the client needs to send a Session packet after successfully binding a resource
569     * for the session.
570     */
571    void sessionsSupported() {
572        sessionSupported = true;
573    }
574
575    /**
576     * Initializes the internal state in order to be able to be reused. The authentication
577     * is used by the connection at the first login and then reused after the connection
578     * is disconnected and then reconnected.
579     */
580    protected void init() {
581        saslNegotiated = false;
582        saslFailed = false;
583        resourceBinded = false;
584        sessionSupported = false;
585    }
586}
587