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.XMPPException.XMPPErrorException; 022import org.jivesoftware.smack.packet.Mechanisms; 023import org.jivesoftware.smack.sasl.SASLAnonymous; 024import org.jivesoftware.smack.sasl.SASLErrorException; 025import org.jivesoftware.smack.sasl.SASLMechanism; 026import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; 027import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; 028 029import javax.security.auth.callback.CallbackHandler; 030 031import java.io.IOException; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.Iterator; 037import java.util.List; 038import java.util.Map; 039import java.util.Set; 040import java.util.logging.Logger; 041 042/** 043 * <p>This class is responsible authenticating the user using SASL, binding the resource 044 * to the connection and establishing a session with the server.</p> 045 * 046 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to 047 * register with the server or authenticate using SASL. If the 048 * server supports SASL then Smack will try to authenticate using SASL..</p> 049 * 050 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box 051 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use 052 * {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms. 053 * 054 * @see org.jivesoftware.smack.sasl.SASLMechanism 055 * 056 * @author Gaston Dombiak 057 * @author Jay Kline 058 */ 059public class SASLAuthentication { 060 061 private static final Logger LOGGER = Logger.getLogger(SASLAuthentication.class.getName()); 062 063 private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<SASLMechanism>(); 064 065 private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<String>(); 066 067 /** 068 * Registers a new SASL mechanism 069 * 070 * @param mechanism a SASLMechanism subclass. 071 */ 072 public static void registerSASLMechanism(SASLMechanism mechanism) { 073 synchronized (REGISTERED_MECHANISMS) { 074 REGISTERED_MECHANISMS.add(mechanism); 075 Collections.sort(REGISTERED_MECHANISMS); 076 } 077 } 078 079 /** 080 * Returns the registered SASLMechanism sorted by the level of preference. 081 * 082 * @return the registered SASLMechanism sorted by the level of preference. 083 */ 084 public static Map<String, String> getRegisterdSASLMechanisms() { 085 Map<String, String> answer = new HashMap<String, String>(); 086 synchronized (REGISTERED_MECHANISMS) { 087 for (SASLMechanism mechanism : REGISTERED_MECHANISMS) { 088 answer.put(mechanism.getClass().getName(), mechanism.getName()); 089 } 090 } 091 return answer; 092 } 093 094 /** 095 * Unregister a SASLMechanism by it's full class name. For example 096 * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism". 097 * 098 * @param clazz the SASLMechanism class's name 099 * @return true if the given SASLMechanism was removed, false otherwise 100 */ 101 public static boolean unregisterSASLMechanism(String clazz) { 102 synchronized (REGISTERED_MECHANISMS) { 103 Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator(); 104 while (it.hasNext()) { 105 SASLMechanism mechanism = it.next(); 106 if (mechanism.getClass().getName().equals(clazz)) { 107 it.remove(); 108 return true; 109 } 110 } 111 } 112 return false; 113 } 114 115 public static boolean blacklistSASLMechanism(String mechansim) { 116 synchronized(BLACKLISTED_MECHANISMS) { 117 return BLACKLISTED_MECHANISMS.add(mechansim); 118 } 119 } 120 121 public static boolean unBlacklistSASLMechanism(String mechanism) { 122 synchronized(BLACKLISTED_MECHANISMS) { 123 return BLACKLISTED_MECHANISMS.remove(mechanism); 124 } 125 } 126 127 public static Set<String> getBlacklistedSASLMechanisms() { 128 synchronized(BLACKLISTED_MECHANISMS) { 129 return new HashSet<String>(BLACKLISTED_MECHANISMS); 130 } 131 } 132 133 private final AbstractXMPPConnection connection; 134 private SASLMechanism currentMechanism = null; 135 136 /** 137 * Boolean indicating if SASL negotiation has finished and was successful. 138 */ 139 private boolean authenticationSuccessful; 140 141 /** 142 * Either of type {@link SmackException} or {@link SASLErrorException} 143 */ 144 private Exception saslException; 145 146 SASLAuthentication(AbstractXMPPConnection connection) { 147 this.connection = connection; 148 this.init(); 149 } 150 151 /** 152 * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users. 153 * 154 * @return true if the server offered ANONYMOUS SASL as a way to authenticate users. 155 */ 156 public boolean hasAnonymousAuthentication() { 157 return serverMechanisms().contains("ANONYMOUS"); 158 } 159 160 /** 161 * Returns true if the server offered SASL authentication besides ANONYMOUS SASL. 162 * 163 * @return true if the server offered SASL authentication besides ANONYMOUS SASL. 164 */ 165 public boolean hasNonAnonymousAuthentication() { 166 return !serverMechanisms().isEmpty() && (serverMechanisms().size() != 1 || !hasAnonymousAuthentication()); 167 } 168 169 /** 170 * Performs SASL authentication of the specified user. If SASL authentication was successful 171 * then resource binding and session establishment will be performed. This method will return 172 * the full JID provided by the server while binding a resource to the connection.<p> 173 * 174 * The server may assign a full JID with a username or resource different than the requested 175 * by this method. 176 * 177 * @param resource the desired resource. 178 * @param cbh the CallbackHandler used to get information from the user 179 * @throws IOException 180 * @throws XMPPErrorException 181 * @throws SASLErrorException 182 * @throws SmackException 183 */ 184 public void authenticate(String resource, CallbackHandler cbh) throws IOException, 185 XMPPErrorException, SASLErrorException, SmackException { 186 SASLMechanism selectedMechanism = selectMechanism(); 187 if (selectedMechanism != null) { 188 currentMechanism = selectedMechanism; 189 synchronized (this) { 190 currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh); 191 try { 192 // Wait until SASL negotiation finishes 193 wait(connection.getPacketReplyTimeout()); 194 } 195 catch (InterruptedException e) { 196 // Ignore 197 } 198 } 199 200 maybeThrowException(); 201 202 if (!authenticationSuccessful) { 203 throw NoResponseException.newWith(connection); 204 } 205 } 206 else { 207 throw new SmackException( 208 "SASL Authentication failed. No known authentication mechanisims."); 209 } 210 } 211 212 /** 213 * Performs SASL authentication of the specified user. If SASL authentication was successful 214 * then resource binding and session establishment will be performed. This method will return 215 * the full JID provided by the server while binding a resource to the connection.<p> 216 * 217 * The server may assign a full JID with a username or resource different than the requested 218 * by this method. 219 * 220 * @param username the username that is authenticating with the server. 221 * @param password the password to send to the server. 222 * @param resource the desired resource. 223 * @throws XMPPErrorException 224 * @throws SASLErrorException 225 * @throws IOException 226 * @throws SmackException 227 */ 228 public void authenticate(String username, String password, String resource) 229 throws XMPPErrorException, SASLErrorException, IOException, 230 SmackException { 231 SASLMechanism selectedMechanism = selectMechanism(); 232 if (selectedMechanism != null) { 233 currentMechanism = selectedMechanism; 234 235 synchronized (this) { 236 currentMechanism.authenticate(username, connection.getHost(), 237 connection.getServiceName(), password); 238 try { 239 // Wait until SASL negotiation finishes 240 wait(connection.getPacketReplyTimeout()); 241 } 242 catch (InterruptedException e) { 243 // Ignore 244 } 245 } 246 247 maybeThrowException(); 248 249 if (!authenticationSuccessful) { 250 throw NoResponseException.newWith(connection); 251 } 252 } 253 else { 254 throw new SmackException( 255 "SASL Authentication failed. No known authentication mechanisims."); 256 } 257 } 258 259 /** 260 * Performs ANONYMOUS SASL authentication. If SASL authentication was successful 261 * then resource binding and session establishment will be performed. This method will return 262 * the full JID provided by the server while binding a resource to the connection.<p> 263 * 264 * The server will assign a full JID with a randomly generated resource and possibly with 265 * no username. 266 * 267 * @throws SASLErrorException 268 * @throws XMPPErrorException if an error occures while authenticating. 269 * @throws SmackException if there was no response from the server. 270 */ 271 public void authenticateAnonymously() throws SASLErrorException, 272 SmackException, XMPPErrorException { 273 currentMechanism = (new SASLAnonymous()).instanceForAuthentication(connection); 274 275 // Wait until SASL negotiation finishes 276 synchronized (this) { 277 currentMechanism.authenticate(null, null, null, ""); 278 try { 279 wait(connection.getPacketReplyTimeout()); 280 } 281 catch (InterruptedException e) { 282 // Ignore 283 } 284 } 285 286 maybeThrowException(); 287 288 if (!authenticationSuccessful) { 289 throw NoResponseException.newWith(connection); 290 } 291 } 292 293 private void maybeThrowException() throws SmackException, SASLErrorException { 294 if (saslException != null){ 295 if (saslException instanceof SmackException) { 296 throw (SmackException) saslException; 297 } else if (saslException instanceof SASLErrorException) { 298 throw (SASLErrorException) saslException; 299 } else { 300 throw new IllegalStateException("Unexpected exception type" , saslException); 301 } 302 } 303 } 304 305 /** 306 * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set 307 * to <code>false</code>. 308 * 309 * @param challenge a base64 encoded string representing the challenge. 310 * @throws SmackException 311 */ 312 public void challengeReceived(String challenge) throws SmackException { 313 challengeReceived(challenge, false); 314 } 315 316 /** 317 * The server is challenging the SASL authentication we just sent. Forward the challenge 318 * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to 319 * the server. The length of the challenge-response sequence varies according to the 320 * SASLMechanism in use. 321 * 322 * @param challenge a base64 encoded string representing the challenge. 323 * @param finalChallenge true if this is the last challenge send by the server within the success stanza 324 * @throws SmackException 325 */ 326 public void challengeReceived(String challenge, boolean finalChallenge) throws SmackException { 327 try { 328 currentMechanism.challengeReceived(challenge, finalChallenge); 329 } catch (SmackException e) { 330 authenticationFailed(e); 331 throw e; 332 } 333 } 334 335 /** 336 * Notification message saying that SASL authentication was successful. The next step 337 * would be to bind the resource. 338 * @throws SmackException 339 */ 340 public void authenticated(Success success) throws SmackException { 341 // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP 342 // "receiving entity") can include "additional data with success" if appropriate for the 343 // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML 344 // character data of the <success/> element." The used SASL mechanism should be able to 345 // verify the data send by the server in the success stanza, if any. 346 if (success.getData() != null) { 347 challengeReceived(success.getData(), true); 348 } 349 currentMechanism.checkIfSuccessfulOrThrow(); 350 authenticationSuccessful = true; 351 // Wake up the thread that is waiting in the #authenticate method 352 synchronized (this) { 353 notify(); 354 } 355 } 356 357 /** 358 * Notification message saying that SASL authentication has failed. The server may have 359 * closed the connection depending on the number of possible retries. 360 * 361 * @param saslFailure the SASL failure as reported by the server 362 * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a> 363 */ 364 public void authenticationFailed(SASLFailure saslFailure) { 365 authenticationFailed(new SASLErrorException(currentMechanism.getName(), saslFailure)); 366 } 367 368 public void authenticationFailed(Exception exception) { 369 saslException = exception; 370 // Wake up the thread that is waiting in the #authenticate method 371 synchronized (this) { 372 notify(); 373 } 374 } 375 376 public boolean authenticationSuccessful() { 377 return authenticationSuccessful; 378 } 379 380 /** 381 * Initializes the internal state in order to be able to be reused. The authentication 382 * is used by the connection at the first login and then reused after the connection 383 * is disconnected and then reconnected. 384 */ 385 protected void init() { 386 authenticationSuccessful = false; 387 saslException = null; 388 } 389 390 private SASLMechanism selectMechanism() { 391 // Locate the SASLMechanism to use 392 SASLMechanism selectedMechanism = null; 393 Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator(); 394 // Iterate in SASL Priority order over registered mechanisms 395 while (it.hasNext()) { 396 SASLMechanism mechanism = it.next(); 397 String mechanismName = mechanism.getName(); 398 synchronized (BLACKLISTED_MECHANISMS) { 399 if (BLACKLISTED_MECHANISMS.contains(mechanismName)) { 400 continue; 401 } 402 } 403 if (serverMechanisms().contains(mechanismName)) { 404 // Create a new instance of the SASLMechanism for every authentication attempt. 405 selectedMechanism = mechanism.instanceForAuthentication(connection); 406 break; 407 } 408 } 409 return selectedMechanism; 410 } 411 412 private List<String> serverMechanisms() { 413 Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE); 414 if (mechanisms == null) { 415 LOGGER.warning("Server did not report any SASL mechanisms"); 416 return Collections.emptyList(); 417 } 418 return mechanisms.getMechanisms(); 419 } 420}