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