blob: 66ff69344e8bc65bec5e6020d386c9412dd5cef3 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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}