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