001/** 002 * 003 * Copyright 2003-2007 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smack; 019 020import org.jivesoftware.smack.SmackException.NoResponseException; 021import org.jivesoftware.smack.SmackException.NotConnectedException; 022import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException; 023import org.jivesoftware.smack.XMPPException.XMPPErrorException; 024import org.jivesoftware.smack.packet.Packet; 025import org.jivesoftware.smack.sasl.SASLAnonymous; 026import org.jivesoftware.smack.sasl.SASLCramMD5Mechanism; 027import org.jivesoftware.smack.sasl.SASLDigestMD5Mechanism; 028import org.jivesoftware.smack.sasl.SASLErrorException; 029import org.jivesoftware.smack.sasl.SASLExternalMechanism; 030import org.jivesoftware.smack.sasl.SASLGSSAPIMechanism; 031import org.jivesoftware.smack.sasl.SASLMechanism; 032import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure; 033import org.jivesoftware.smack.sasl.SASLPlainMechanism; 034 035import javax.security.auth.callback.CallbackHandler; 036import javax.security.sasl.SaslException; 037 038import java.io.IOException; 039import java.lang.reflect.Constructor; 040import java.util.ArrayList; 041import java.util.Collection; 042import java.util.HashMap; 043import java.util.List; 044import java.util.Map; 045 046/** 047 * <p>This class is responsible authenticating the user using SASL, binding the resource 048 * to the connection and establishing a session with the server.</p> 049 * 050 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to 051 * register with the server, authenticate using Non-SASL or authenticate using SASL. If the 052 * server supports SASL then Smack will first try to authenticate using SASL. But if that 053 * fails then Non-SASL will be tried.</p> 054 * 055 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box 056 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use 057 * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered 058 * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default, 059 * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p> 060 * 061 * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for 062 * the connection. If no resource is passed in {@link #authenticate(String, String, String)} 063 * then the server will assign a resource for the connection. In case a resource is passed 064 * then the server will receive the desired resource but may assign a modified resource for 065 * the connection.</p> 066 * 067 * <p>Once a resource has been binded and if the server supports sessions then Smack will establish 068 * a session so that instant messaging and presence functionalities may be used.</p> 069 * 070 * @see org.jivesoftware.smack.sasl.SASLMechanism 071 * 072 * @author Gaston Dombiak 073 * @author Jay Kline 074 */ 075public class SASLAuthentication { 076 077 private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>(); 078 private static List<String> mechanismsPreferences = new ArrayList<String>(); 079 080 private XMPPConnection connection; 081 private Collection<String> serverMechanisms = new ArrayList<String>(); 082 private SASLMechanism currentMechanism = null; 083 /** 084 * Boolean indicating if SASL negotiation has finished and was successful. 085 */ 086 private boolean saslNegotiated; 087 088 /** 089 * The SASL related error condition if there was one provided by the server. 090 */ 091 private SASLFailure saslFailure; 092 093 static { 094 095 // Register SASL mechanisms supported by Smack 096 registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class); 097 registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class); 098 registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class); 099 registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class); 100 registerSASLMechanism("PLAIN", SASLPlainMechanism.class); 101 registerSASLMechanism("ANONYMOUS", SASLAnonymous.class); 102 103 supportSASLMechanism("GSSAPI",0); 104 supportSASLMechanism("DIGEST-MD5",1); 105 supportSASLMechanism("CRAM-MD5",2); 106 supportSASLMechanism("PLAIN",3); 107 supportSASLMechanism("ANONYMOUS",4); 108 109 } 110 111 /** 112 * Registers a new SASL mechanism 113 * 114 * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. 115 * @param mClass a SASLMechanism subclass. 116 */ 117 public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) { 118 implementedMechanisms.put(name, mClass); 119 } 120 121 /** 122 * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't 123 * be possible to authenticate users using the removed SASL mechanism. It also removes the 124 * mechanism from the supported list. 125 * 126 * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. 127 */ 128 public static void unregisterSASLMechanism(String name) { 129 implementedMechanisms.remove(name); 130 mechanismsPreferences.remove(name); 131 } 132 133 134 /** 135 * Registers a new SASL mechanism in the specified preference position. The client will try 136 * to authenticate using the most prefered SASL mechanism that is also supported by the server. 137 * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)} 138 * 139 * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. 140 */ 141 public static void supportSASLMechanism(String name) { 142 mechanismsPreferences.add(0, name); 143 } 144 145 /** 146 * Registers a new SASL mechanism in the specified preference position. The client will try 147 * to authenticate using the most prefered SASL mechanism that is also supported by the server. 148 * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism. 149 * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be 150 * registered via {@link #registerSASLMechanism(String, Class)} 151 * 152 * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. 153 * @param index preference position amongst all the implemented SASL mechanism. Starts with 0. 154 */ 155 public static void supportSASLMechanism(String name, int index) { 156 mechanismsPreferences.add(index, name); 157 } 158 159 /** 160 * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't 161 * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism 162 * is still registered, but will just not be used. 163 * 164 * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. 165 */ 166 public static void unsupportSASLMechanism(String name) { 167 mechanismsPreferences.remove(name); 168 } 169 170 /** 171 * Returns the registerd SASLMechanism classes sorted by the level of preference. 172 * 173 * @return the registerd SASLMechanism classes sorted by the level of preference. 174 */ 175 public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() { 176 List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>(); 177 for (String mechanismsPreference : mechanismsPreferences) { 178 answer.add(implementedMechanisms.get(mechanismsPreference)); 179 } 180 return answer; 181 } 182 183 SASLAuthentication(XMPPConnection connection) { 184 super(); 185 this.connection = connection; 186 this.init(); 187 } 188 189 /** 190 * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users. 191 * 192 * @return true if the server offered ANONYMOUS SASL as a way to authenticate users. 193 */ 194 public boolean hasAnonymousAuthentication() { 195 return serverMechanisms.contains("ANONYMOUS"); 196 } 197 198 /** 199 * Returns true if the server offered SASL authentication besides ANONYMOUS SASL. 200 * 201 * @return true if the server offered SASL authentication besides ANONYMOUS SASL. 202 */ 203 public boolean hasNonAnonymousAuthentication() { 204 return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication()); 205 } 206 207 /** 208 * Performs SASL authentication of the specified user. If SASL authentication was successful 209 * then resource binding and session establishment will be performed. This method will return 210 * the full JID provided by the server while binding a resource to the connection.<p> 211 * 212 * The server may assign a full JID with a username or resource different than the requested 213 * by this method. 214 * 215 * @param resource the desired resource. 216 * @param cbh the CallbackHandler used to get information from the user 217 * @throws IOException 218 * @throws XMPPErrorException 219 * @throws NoResponseException 220 * @throws SASLErrorException 221 * @throws ResourceBindingNotOfferedException 222 * @throws NotConnectedException 223 */ 224 public void authenticate(String resource, CallbackHandler cbh) throws IOException, 225 NoResponseException, XMPPErrorException, SASLErrorException, ResourceBindingNotOfferedException, NotConnectedException { 226 // Locate the SASLMechanism to use 227 String selectedMechanism = null; 228 for (String mechanism : mechanismsPreferences) { 229 if (implementedMechanisms.containsKey(mechanism) 230 && serverMechanisms.contains(mechanism)) { 231 selectedMechanism = mechanism; 232 break; 233 } 234 } 235 if (selectedMechanism != null) { 236 // A SASL mechanism was found. Authenticate using the selected mechanism and then 237 // proceed to bind a resource 238 Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism); 239 240 Constructor<? extends SASLMechanism> constructor; 241 try { 242 constructor = mechanismClass.getConstructor(SASLAuthentication.class); 243 currentMechanism = constructor.newInstance(this); 244 } 245 catch (Exception e) { 246 throw new SaslException("Exception when creating the SASLAuthentication instance", 247 e); 248 } 249 250 synchronized (this) { 251 // Trigger SASL authentication with the selected mechanism. We use 252 // connection.getHost() since GSAPI requires the FQDN of the server, which 253 // may not match the XMPP domain. 254 currentMechanism.authenticate(connection.getHost(), cbh); 255 try { 256 // Wait until SASL negotiation finishes 257 wait(connection.getPacketReplyTimeout()); 258 } 259 catch (InterruptedException e) { 260 // Ignore 261 } 262 } 263 264 if (saslFailure != null) { 265 // SASL authentication failed and the server may have closed the connection 266 // so throw an exception 267 throw new SASLErrorException(selectedMechanism, saslFailure); 268 } 269 270 if (!saslNegotiated) { 271 throw new NoResponseException(); 272 } 273 } 274 else { 275 throw new SaslException( 276 "SASL Authentication failed. No known authentication mechanisims."); 277 } 278 } 279 280 /** 281 * Performs SASL authentication of the specified user. If SASL authentication was successful 282 * then resource binding and session establishment will be performed. This method will return 283 * the full JID provided by the server while binding a resource to the connection.<p> 284 * 285 * The server may assign a full JID with a username or resource different than the requested 286 * by this method. 287 * 288 * @param username the username that is authenticating with the server. 289 * @param password the password to send to the server. 290 * @param resource the desired resource. 291 * @throws XMPPErrorException 292 * @throws SASLErrorException 293 * @throws IOException 294 * @throws SaslException 295 * @throws SmackException 296 */ 297 public void authenticate(String username, String password, String resource) 298 throws XMPPErrorException, SASLErrorException, SaslException, IOException, 299 SmackException { 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 Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism); 313 try { 314 Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class); 315 currentMechanism = constructor.newInstance(this); 316 } 317 catch (Exception e) { 318 throw new SaslException("Exception when creating the SASLAuthentication instance", 319 e); 320 } 321 322 synchronized (this) { 323 // Trigger SASL authentication with the selected mechanism. We use 324 // connection.getHost() since GSAPI requires the FQDN of the server, which 325 // may not match the XMPP domain. 326 327 // The serviceName is basically the value that XMPP server sends to the client as 328 // being 329 // the location of the XMPP service we are trying to connect to. This should have 330 // the 331 // format: host ["/" serv-name ] as per RFC-2831 guidelines 332 String serviceName = connection.getServiceName(); 333 currentMechanism.authenticate(username, connection.getHost(), serviceName, password); 334 335 try { 336 // Wait until SASL negotiation finishes 337 wait(connection.getPacketReplyTimeout()); 338 } 339 catch (InterruptedException e) { 340 // Ignore 341 } 342 343 } 344 345 if (saslFailure != null) { 346 // SASL authentication failed and the server may have closed the connection 347 // so throw an exception 348 throw new SASLErrorException(selectedMechanism, saslFailure); 349 } 350 351 if (!saslNegotiated) { 352 throw new NoResponseException(); 353 } 354 } 355 else { 356 throw new SaslException( 357 "SASL Authentication failed. No known authentication mechanisims."); 358 } 359 } 360 361 /** 362 * Performs ANONYMOUS SASL authentication. If SASL authentication was successful 363 * then resource binding and session establishment will be performed. This method will return 364 * the full JID provided by the server while binding a resource to the connection.<p> 365 * 366 * The server will assign a full JID with a randomly generated resource and possibly with 367 * no username. 368 * 369 * @throws SASLErrorException 370 * @throws IOException 371 * @throws SaslException 372 * @throws XMPPErrorException if an error occures while authenticating. 373 * @throws SmackException if there was no response from the server. 374 */ 375 public void authenticateAnonymously() throws SASLErrorException, SaslException, IOException, 376 SmackException, XMPPErrorException { 377 currentMechanism = new SASLAnonymous(this); 378 379 // Wait until SASL negotiation finishes 380 synchronized (this) { 381 currentMechanism.authenticate(null, null, null, ""); 382 try { 383 wait(connection.getPacketReplyTimeout()); 384 } 385 catch (InterruptedException e) { 386 // Ignore 387 } 388 } 389 390 if (saslFailure != null) { 391 // SASL authentication failed and the server may have closed the connection 392 // so throw an exception 393 throw new SASLErrorException(currentMechanism.toString(), saslFailure); 394 } 395 396 if (!saslNegotiated) { 397 throw new NoResponseException(); 398 } 399 } 400 401 /** 402 * Sets the available SASL mechanism reported by the server. The server will report the 403 * available SASL mechanism once the TLS negotiation was successful. This information is 404 * stored and will be used when doing the authentication for logging in the user. 405 * 406 * @param mechanisms collection of strings with the available SASL mechanism reported 407 * by the server. 408 */ 409 public void setAvailableSASLMethods(Collection<String> mechanisms) { 410 this.serverMechanisms = mechanisms; 411 } 412 413 /** 414 * Returns true if the user was able to authenticate with the server usins SASL. 415 * 416 * @return true if the user was able to authenticate with the server usins SASL. 417 */ 418 public boolean isAuthenticated() { 419 return saslNegotiated; 420 } 421 422 /** 423 * The server is challenging the SASL authentication we just sent. Forward the challenge 424 * to the current SASLMechanism we are using. The SASLMechanism will send a response to 425 * the server. The length of the challenge-response sequence varies according to the 426 * SASLMechanism in use. 427 * 428 * @param challenge a base64 encoded string representing the challenge. 429 * @throws IOException If a network error occures while authenticating. 430 * @throws NotConnectedException 431 */ 432 public void challengeReceived(String challenge) throws IOException, NotConnectedException { 433 currentMechanism.challengeReceived(challenge); 434 } 435 436 /** 437 * Notification message saying that SASL authentication was successful. The next step 438 * would be to bind the resource. 439 */ 440 public void authenticated() { 441 saslNegotiated = true; 442 // Wake up the thread that is waiting in the #authenticate method 443 synchronized (this) { 444 notify(); 445 } 446 } 447 448 /** 449 * Notification message saying that SASL authentication has failed. The server may have 450 * closed the connection depending on the number of possible retries. 451 * 452 * @param saslFailure the SASL failure as reported by the server 453 * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a> 454 */ 455 public void authenticationFailed(SASLFailure saslFailure) { 456 this.saslFailure = saslFailure; 457 // Wake up the thread that is waiting in the #authenticate method 458 synchronized (this) { 459 notify(); 460 } 461 } 462 463 public void send(Packet stanza) throws NotConnectedException { 464 connection.sendPacket(stanza); 465 } 466 467 468 /** 469 * Initializes the internal state in order to be able to be reused. The authentication 470 * is used by the connection at the first login and then reused after the connection 471 * is disconnected and then reconnected. 472 */ 473 protected void init() { 474 saslNegotiated = false; 475 saslFailure = null; 476 } 477}