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 InterruptedException if the calling thread was interrupted. 254 * @throws NotConnectedException if the XMPP connection is not connected. 255 * @throws SmackSaslException if a SASL specific error occurred. 256 */ 257 void authenticated(Success success) throws InterruptedException, SmackSaslException, NotConnectedException { 258 // RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP 259 // "receiving entity") can include "additional data with success" if appropriate for the 260 // SASL mechanism in use. In XMPP, this is done by including the additional data as the XML 261 // character data of the <success/> element." The used SASL mechanism should be able to 262 // verify the data send by the server in the success stanza, if any. 263 if (success.getData() != null) { 264 challengeReceived(success.getData(), true); 265 } 266 267 // Wake up the thread that is waiting in the #authenticate method 268 synchronized (this) { 269 currentMechanism.afterFinalSaslChallenge(); 270 271 notify(); 272 } 273 } 274 275 /** 276 * Notification message saying that SASL authentication has failed. The server may have 277 * closed the connection depending on the number of possible retries. 278 * 279 * @param saslFailure the SASL failure as reported by the server 280 * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a> 281 */ 282 void authenticationFailed(SASLFailure saslFailure) { 283 SASLErrorException saslErrorException; 284 synchronized (this) { 285 saslErrorException = new SASLErrorException(currentMechanism.getName(), saslFailure); 286 } 287 authenticationFailed(saslErrorException); 288 } 289 290 void authenticationFailed(Exception exception) { 291 // Wake up the thread that is waiting in the #authenticate method 292 synchronized (this) { 293 currentMechanism.setException(exception); 294 notify(); 295 } 296 } 297 298 public boolean authenticationSuccessful() { 299 synchronized (this) { 300 if (currentMechanism == null) { 301 return false; 302 } 303 return currentMechanism.isAuthenticationSuccessful(); 304 } 305 } 306 307 String getNameOfLastUsedSaslMechansism() { 308 SASLMechanism lastUsedMech = currentMechanism; 309 if (lastUsedMech == null) { 310 return null; 311 } 312 return lastUsedMech.getName(); 313 } 314 315 private SASLMechanism selectMechanism(EntityBareJid authzid, String password) throws SmackException.SmackSaslException { 316 final boolean passwordAvailable = StringUtils.isNotEmpty(password); 317 318 Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator(); 319 final List<String> serverMechanisms = getServerMechanisms(); 320 if (serverMechanisms.isEmpty()) { 321 LOGGER.warning("Server did not report any SASL mechanisms"); 322 } 323 324 List<String> skipReasons = new ArrayList<>(); 325 326 // Iterate in SASL Priority order over registered mechanisms 327 while (it.hasNext()) { 328 SASLMechanism mechanism = it.next(); 329 String mechanismName = mechanism.getName(); 330 331 if (!serverMechanisms.contains(mechanismName)) { 332 continue; 333 } 334 335 synchronized (BLACKLISTED_MECHANISMS) { 336 if (BLACKLISTED_MECHANISMS.contains(mechanismName)) { 337 continue; 338 } 339 } 340 341 if (!configuration.isEnabledSaslMechanism(mechanismName)) { 342 continue; 343 } 344 345 if (authzid != null && !mechanism.authzidSupported()) { 346 skipReasons.add("Skipping " + mechanism + " because authzid is required by not supported by this SASL mechanism"); 347 continue; 348 } 349 350 if (mechanism.requiresPassword() && !passwordAvailable) { 351 skipReasons.add("Skipping " + mechanism + " because a password is required for it, but none was provided to the connection configuration"); 352 continue; 353 } 354 355 // Create a new instance of the SASLMechanism for every authentication attempt. 356 return mechanism.instanceForAuthentication(connection, configuration); 357 } 358 359 synchronized (BLACKLISTED_MECHANISMS) { 360 // @formatter:off 361 throw new SmackException.SmackSaslException( 362 "No supported and enabled SASL Mechanism provided by server. " + 363 "Server announced mechanisms: " + serverMechanisms + ". " + 364 "Registered SASL mechanisms with Smack: " + REGISTERED_MECHANISMS + ". " + 365 "Enabled SASL mechanisms for this connection: " + configuration.getEnabledSaslMechanisms() + ". " + 366 "Blacklisted SASL mechanisms: " + BLACKLISTED_MECHANISMS + ". " + 367 "Skip reasons: " + skipReasons 368 ); 369 // @formatter;on 370 } 371 } 372 373 private List<String> getServerMechanisms() { 374 Mechanisms mechanisms = connection.getFeature(Mechanisms.class); 375 if (mechanisms == null) { 376 return Collections.emptyList(); 377 } 378 return mechanisms.getMechanisms(); 379 } 380}