• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
21 package org.jivesoftware.smack;
22 
23 import org.jivesoftware.smack.filter.PacketIDFilter;
24 import org.jivesoftware.smack.packet.Bind;
25 import org.jivesoftware.smack.packet.IQ;
26 import org.jivesoftware.smack.packet.Packet;
27 import org.jivesoftware.smack.packet.Session;
28 import org.jivesoftware.smack.sasl.*;
29 
30 import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
31 import java.io.IOException;
32 import java.lang.reflect.Constructor;
33 import 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  */
64 public 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",0);
100 //        supportSASLMechanism("CRAM-MD5",2);
101         supportSASLMechanism("PLAIN",1);
102         supportSASLMechanism("ANONYMOUS",2);
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      */
registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass)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      */
unregisterSASLMechanism(String name)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      */
supportSASLMechanism(String name)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      */
supportSASLMechanism(String name, int index)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      */
unsupportSASLMechanism(String name)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      */
getRegisterSASLMechanisms()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 
SASLAuthentication(Connection connection)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      */
hasAnonymousAuthentication()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      */
hasNonAnonymousAuthentication()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      */
authenticate(String username, String resource, CallbackHandler cbh)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      */
authenticate(String username, String password, String resource)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      */
authenticateAnonymously()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 
bindResourceAndEstablishSession(String resource)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      */
setAvailableSASLMethods(Collection<String> mechanisms)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      */
isAuthenticated()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      */
challengeReceived(String challenge)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      */
authenticated()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      */
authenticationFailed()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      */
authenticationFailed(String condition)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      */
bindingRequired()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 
send(Packet stanza)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      */
sessionsSupported()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      */
init()580     protected void init() {
581         saslNegotiated = false;
582         saslFailed = false;
583         resourceBinded = false;
584         sessionSupported = false;
585     }
586 }
587