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; 045 046import org.jxmpp.jid.DomainBareJid; 047import org.jxmpp.jid.EntityBareJid; 048 049/** 050 * <p>This class is responsible authenticating the user using SASL, binding the resource 051 * to the connection and establishing a session with the server.</p> 052 * 053 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to 054 * register with the server or authenticate using SASL. If the 055 * server supports SASL then Smack will try to authenticate using SASL..</p> 056 * 057 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box 058 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use 059 * {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms. 060 * 061 * @see org.jivesoftware.smack.sasl.SASLMechanism 062 * 063 * @author Gaston Dombiak 064 * @author Jay Kline 065 */ 066public final class SASLAuthentication { 067 068 private static final Logger LOGGER = Logger.getLogger(SASLAuthentication.class.getName()); 069 070 private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<>(); 071 072 private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<>(); 073 074 static { 075 // Blacklist SCRAM-SHA-1-PLUS for now. 076 blacklistSASLMechanism(ScramSha1PlusMechanism.NAME); 077 } 078 079 /** 080 * Registers a new SASL mechanism. 081 * 082 * @param mechanism a SASLMechanism subclass. 083 */ 084 public static void registerSASLMechanism(SASLMechanism mechanism) { 085 synchronized (REGISTERED_MECHANISMS) { 086 REGISTERED_MECHANISMS.add(mechanism); 087 Collections.sort(REGISTERED_MECHANISMS); 088 } 089 } 090 091 /** 092 * Returns the registered SASLMechanism sorted by the level of preference. 093 * 094 * @return the registered SASLMechanism sorted by the level of preference. 095 */ 096 public static Map<String, String> getRegisterdSASLMechanisms() { 097 Map<String, String> answer = new LinkedHashMap<>(); 098 synchronized (REGISTERED_MECHANISMS) { 099 for (SASLMechanism mechanism : REGISTERED_MECHANISMS) { 100 answer.put(mechanism.getClass().getName(), mechanism.toString()); 101 } 102 } 103 return answer; 104 } 105 106 public static boolean isSaslMechanismRegistered(String saslMechanism) { 107 synchronized (REGISTERED_MECHANISMS) { 108 for (SASLMechanism mechanism : REGISTERED_MECHANISMS) { 109 if (mechanism.getName().equals(saslMechanism)) { 110 return true; 111 } 112 } 113 } 114 return false; 115 } 116 117 /** 118 * Unregister a SASLMechanism by it's full class name. For example 119 * "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism". 120 * 121 * @param clazz the SASLMechanism class's name 122 * @return true if the given SASLMechanism was removed, false otherwise 123 */ 124 public static boolean unregisterSASLMechanism(String clazz) { 125 synchronized (REGISTERED_MECHANISMS) { 126 Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator(); 127 while (it.hasNext()) { 128 SASLMechanism mechanism = it.next(); 129 if (mechanism.getClass().getName().equals(clazz)) { 130 it.remove(); 131 return true; 132 } 133 } 134 } 135 return false; 136 } 137 138 public static boolean blacklistSASLMechanism(String mechanism) { 139 synchronized (BLACKLISTED_MECHANISMS) { 140 return BLACKLISTED_MECHANISMS.add(mechanism); 141 } 142 } 143 144 public static boolean unBlacklistSASLMechanism(String mechanism) { 145 synchronized (BLACKLISTED_MECHANISMS) { 146 return BLACKLISTED_MECHANISMS.remove(mechanism); 147 } 148 } 149 150 public static Set<String> getBlacklistedSASLMechanisms() { 151 return Collections.unmodifiableSet(BLACKLISTED_MECHANISMS); 152 } 153 154 private final AbstractXMPPConnection connection; 155 private final ConnectionConfiguration configuration; 156 private SASLMechanism currentMechanism = null; 157 158 SASLAuthentication(AbstractXMPPConnection connection, ConnectionConfiguration configuration) { 159 this.configuration = configuration; 160 this.connection = connection; 161 } 162 163 /** 164 * Performs SASL authentication of the specified user. If SASL authentication was successful 165 * then resource binding and session establishment will be performed. This method will return 166 * the full JID provided by the server while binding a resource to the connection.<p> 167 * 168 * The server may assign a full JID with a username or resource different than the requested 169 * by this method. 170 * 171 * @param username the username that is authenticating with the server. 172 * @param password the password to send to the server. 173 * @param authzid the authorization identifier (typically null). 174 * @param sslSession the optional SSL/TLS session (if one was established) 175 * @return the used SASLMechanism. 176 * @throws XMPPErrorException if there was an XMPP error returned. 177 * @throws SASLErrorException if a SASL protocol error was returned. 178 * @throws IOException if an I/O error occurred. 179 * @throws InterruptedException if the calling thread was interrupted. 180 * @throws SmackSaslException if a SASL specific error occurred. 181 * @throws NotConnectedException if the XMPP connection is not connected. 182 * @throws NoResponseException if there was no response from the remote entity. 183 */ 184 SASLMechanism authenticate(String username, String password, EntityBareJid authzid, SSLSession sslSession) 185 throws XMPPErrorException, SASLErrorException, IOException, 186 InterruptedException, SmackSaslException, NotConnectedException, NoResponseException { 187 final SASLMechanism mechanism = selectMechanism(authzid); 188 final CallbackHandler callbackHandler = configuration.getCallbackHandler(); 189 final String host = connection.getHost(); 190 final DomainBareJid xmppServiceDomain = connection.getXMPPServiceDomain(); 191 192 synchronized (this) { 193 currentMechanism = mechanism; 194 195 if (callbackHandler != null) { 196 currentMechanism.authenticate(host, xmppServiceDomain, callbackHandler, authzid, sslSession); 197 } 198 else { 199 currentMechanism.authenticate(username, host, xmppServiceDomain, password, authzid, sslSession); 200 } 201 202 final long deadline = System.currentTimeMillis() + connection.getReplyTimeout(); 203 while (!mechanism.isFinished()) { 204 final long now = System.currentTimeMillis(); 205 if (now >= deadline) break; 206 // Wait until SASL negotiation finishes 207 wait(deadline - now); 208 } 209 } 210 211 mechanism.throwExceptionIfRequired(); 212 213 return mechanism; 214 } 215 216 /** 217 * Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set 218 * to <code>false</code>. 219 * 220 * @param challenge the challenge Nonza. 221 * @throws SmackException if Smack detected an exceptional situation. 222 * @throws InterruptedException if the calling thread was interrupted. 223 */ 224 void challengeReceived(SaslNonza.Challenge challenge) throws SmackException, InterruptedException { 225 challengeReceived(challenge.getData(), false); 226 } 227 228 /** 229 * The server is challenging the SASL authentication we just sent. Forward the challenge 230 * to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to 231 * the server. The length of the challenge-response sequence varies according to the 232 * SASLMechanism in use. 233 * 234 * @param challenge a base64 encoded string representing the challenge. 235 * @param finalChallenge true if this is the last challenge send by the server within the success stanza 236 * @throws SmackSaslException if a SASL specific error occurred. 237 * @throws NotConnectedException if the XMPP connection is not connected. 238 * @throws InterruptedException if the calling thread was interrupted. 239 */ 240 private void challengeReceived(String challenge, boolean finalChallenge) throws SmackSaslException, NotConnectedException, InterruptedException { 241 SASLMechanism mechanism; 242 synchronized (this) { 243 mechanism = currentMechanism; 244 } 245 mechanism.challengeReceived(challenge, finalChallenge); 246 } 247 248 /** 249 * Notification message saying that SASL authentication was successful. The next step 250 * would be to bind the resource. 251 * @param success result of the authentication. 252 * @throws SmackException if Smack detected an exceptional situation. 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) throws SmackException.SmackSaslException { 316 Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator(); 317 final List<String> serverMechanisms = getServerMechanisms(); 318 if (serverMechanisms.isEmpty()) { 319 LOGGER.warning("Server did not report any SASL mechanisms"); 320 } 321 // Iterate in SASL Priority order over registered mechanisms 322 while (it.hasNext()) { 323 SASLMechanism mechanism = it.next(); 324 String mechanismName = mechanism.getName(); 325 synchronized (BLACKLISTED_MECHANISMS) { 326 if (BLACKLISTED_MECHANISMS.contains(mechanismName)) { 327 continue; 328 } 329 } 330 if (!configuration.isEnabledSaslMechanism(mechanismName)) { 331 continue; 332 } 333 if (authzid != null) { 334 if (!mechanism.authzidSupported()) { 335 LOGGER.fine("Skipping " + mechanism + " because authzid is required by not supported by this SASL mechanism"); 336 continue; 337 } 338 } 339 if (serverMechanisms.contains(mechanismName)) { 340 // Create a new instance of the SASLMechanism for every authentication attempt. 341 return mechanism.instanceForAuthentication(connection, configuration); 342 } 343 } 344 345 synchronized (BLACKLISTED_MECHANISMS) { 346 // @formatter:off 347 throw new SmackException.SmackSaslException( 348 "No supported and enabled SASL Mechanism provided by server. " + 349 "Server announced mechanisms: " + serverMechanisms + ". " + 350 "Registered SASL mechanisms with Smack: " + REGISTERED_MECHANISMS + ". " + 351 "Enabled SASL mechanisms for this connection: " + configuration.getEnabledSaslMechanisms() + ". " + 352 "Blacklisted SASL mechanisms: " + BLACKLISTED_MECHANISMS + '.' 353 ); 354 // @formatter;on 355 } 356 } 357 358 private List<String> getServerMechanisms() { 359 Mechanisms mechanisms = connection.getFeature(Mechanisms.class); 360 if (mechanisms == null) { 361 return Collections.emptyList(); 362 } 363 return mechanisms.getMechanisms(); 364 } 365}