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 */ 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