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