001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2014-2021 Florian Schmaus 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 */ 017package org.jivesoftware.smack.sasl; 018 019import java.text.Normalizer; 020import java.text.Normalizer.Form; 021 022import javax.net.ssl.SSLSession; 023import javax.security.auth.callback.CallbackHandler; 024 025import org.jivesoftware.smack.ConnectionConfiguration; 026import org.jivesoftware.smack.SmackException.NoResponseException; 027import org.jivesoftware.smack.SmackException.NotConnectedException; 028import org.jivesoftware.smack.SmackException.SmackSaslException; 029import org.jivesoftware.smack.XMPPConnection; 030import org.jivesoftware.smack.sasl.packet.SaslNonza.AuthMechanism; 031import org.jivesoftware.smack.sasl.packet.SaslNonza.Response; 032import org.jivesoftware.smack.util.StringUtils; 033import org.jivesoftware.smack.util.stringencoder.Base64; 034 035import org.jxmpp.jid.DomainBareJid; 036import org.jxmpp.jid.EntityBareJid; 037 038/** 039 * Base class for SASL mechanisms. 040 * Subclasses will likely want to implement their own versions of these methods: 041 * <ul> 042 * <li>{@link #authenticate(String, String, DomainBareJid, String, EntityBareJid, SSLSession)} -- Initiate authentication stanza using the 043 * deprecated method.</li> 044 * <li>{@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} -- Initiate authentication stanza 045 * using the CallbackHandler method.</li> 046 * <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li> 047 * </ul> 048 * 049 * @author Jay Kline 050 * @author Florian Schmaus 051 */ 052public abstract class SASLMechanism implements Comparable<SASLMechanism> { 053 054 public static final String CRAMMD5 = "CRAM-MD5"; 055 public static final String DIGESTMD5 = "DIGEST-MD5"; 056 public static final String EXTERNAL = "EXTERNAL"; 057 public static final String GSSAPI = "GSSAPI"; 058 public static final String PLAIN = "PLAIN"; 059 060 /** 061 * Boolean indicating if SASL negotiation has finished and was successful. 062 */ 063 private boolean authenticationSuccessful; 064 065 /** 066 * Either of type {@link SmackSaslException},{@link SASLErrorException}, {@link NotConnectedException} or 067 * {@link InterruptedException}. 068 */ 069 private Exception exception; 070 071 protected XMPPConnection connection; 072 073 protected ConnectionConfiguration connectionConfiguration; 074 075 /** 076 * Then authentication identity (authcid). RFC 6120 § 6.3.7 informs us that some SASL mechanisms use this as a 077 * "simple user name". But the exact form is a matter of the mechanism and that it does not necessarily map to an 078 * localpart. But it usually is the localpart of the client JID, although sometimes other formats are used (e.g. the 079 * full JID). 080 * <p> 081 * Not to be confused with the authzid (see RFC 6120 § 6.3.8). 082 * </p> 083 */ 084 protected String authenticationId; 085 086 /** 087 * The authorization identifier (authzid). 088 * This is always a bare Jid, but can be null. 089 */ 090 protected EntityBareJid authorizationId; 091 092 /** 093 * The name of the XMPP service 094 */ 095 protected DomainBareJid serviceName; 096 097 /** 098 * The users password 099 */ 100 protected String password; 101 protected String host; 102 103 /** 104 * The used SSL/TLS session (if any). 105 */ 106 protected SSLSession sslSession; 107 108 /** 109 * Builds and sends the <code>auth</code> stanza to the server. Note that this method of 110 * authentication is not recommended, since it is very inflexible. Use 111 * {@link #authenticate(String, DomainBareJid, CallbackHandler, EntityBareJid, SSLSession)} whenever possible. 112 * 113 * Explanation of auth stanza: 114 * 115 * The client authentication stanza needs to include the digest-uri of the form: xmpp/serviceName 116 * From RFC-2831: 117 * digest-uri = "digest-uri" "=" digest-uri-value 118 * digest-uri-value = serv-type "/" host [ "/" serv-name ] 119 * 120 * digest-uri: 121 * Indicates the principal name of the service with which the client 122 * wishes to connect, formed from the serv-type, host, and serv-name. 123 * For example, the FTP service 124 * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP 125 * server from the example above would have a "digest-uri" value of 126 * "smtp/mail3.example.com/example.com". 127 * 128 * host: 129 * The DNS host name or IP address for the service requested. The DNS host name 130 * must be the fully-qualified canonical name of the host. The DNS host name is the 131 * preferred form; see notes on server processing of the digest-uri. 132 * 133 * serv-name: 134 * Indicates the name of the service if it is replicated. The service is 135 * considered to be replicated if the client's service-location process involves resolution 136 * using standard DNS lookup operations, and if these operations involve DNS records (such 137 * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case, 138 * the initial name used by the client is the "serv-name", and the final name is the "host" 139 * component. For example, the incoming mail service for "example.com" may be replicated 140 * through the use of MX records stored in the DNS, one of which points at an SMTP server 141 * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be 142 * "mail3.example.com". If the service is not replicated, or the serv-name is identical to 143 * the host, then the serv-name component MUST be omitted 144 * 145 * digest-uri verification is needed for ejabberd 2.0.3 and higher 146 * 147 * @param username the username of the user being authenticated. 148 * @param host the hostname where the user account resides. 149 * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation 150 * serviceName format is: host [ "/" serv-name ] as per RFC-2831 151 * @param password the password for this account. 152 * @param authzid the optional authorization identity. 153 * @param sslSession the optional SSL/TLS session (if one was established) 154 * @throws SmackSaslException if a SASL related error occurs. 155 * @throws NotConnectedException if the XMPP connection is not connected. 156 * @throws InterruptedException if the calling thread was interrupted. 157 */ 158 public final void authenticate(String username, String host, DomainBareJid serviceName, String password, 159 EntityBareJid authzid, SSLSession sslSession) 160 throws SmackSaslException, NotConnectedException, InterruptedException { 161 this.authenticationId = username; 162 this.host = host; 163 this.serviceName = serviceName; 164 this.password = password; 165 this.authorizationId = authzid; 166 this.sslSession = sslSession; 167 assert authorizationId == null || authzidSupported(); 168 authenticateInternal(); 169 authenticate(); 170 } 171 172 protected void authenticateInternal() throws SmackSaslException { 173 } 174 175 /** 176 * Builds and sends the <code>auth</code> stanza to the server. The callback handler will handle 177 * any additional information, such as the authentication ID or realm, if it is needed. 178 * 179 * @param host the hostname where the user account resides. 180 * @param serviceName the xmpp service location 181 * @param cbh the CallbackHandler to obtain user information. 182 * @param authzid the optional authorization identity. 183 * @param sslSession the optional SSL/TLS session (if one was established) 184 * @throws SmackSaslException if a SASL related error occurs. 185 * @throws NotConnectedException if the XMPP connection is not connected. 186 * @throws InterruptedException if the calling thread was interrupted. 187 */ 188 public void authenticate(String host, DomainBareJid serviceName, CallbackHandler cbh, EntityBareJid authzid, SSLSession sslSession) 189 throws SmackSaslException, NotConnectedException, InterruptedException { 190 this.host = host; 191 this.serviceName = serviceName; 192 this.authorizationId = authzid; 193 this.sslSession = sslSession; 194 assert authorizationId == null || authzidSupported(); 195 authenticateInternal(cbh); 196 authenticate(); 197 } 198 199 protected abstract void authenticateInternal(CallbackHandler cbh) throws SmackSaslException; 200 201 private void authenticate() throws SmackSaslException, NotConnectedException, InterruptedException { 202 byte[] authenticationBytes = getAuthenticationText(); 203 String authenticationText; 204 // Some SASL mechanisms do return an empty array (e.g. EXTERNAL from javax), so check that 205 // the array is not-empty. Mechanisms are allowed to return either 'null' or an empty array 206 // if there is no authentication text. 207 if (authenticationBytes != null && authenticationBytes.length > 0) { 208 authenticationText = Base64.encodeToString(authenticationBytes); 209 } else { 210 // RFC6120 6.4.2 "If the initiating entity needs to send a zero-length initial response, 211 // it MUST transmit the response as a single equals sign character ("="), which 212 // indicates that the response is present but contains no data." 213 authenticationText = "="; 214 } 215 // Send the authentication to the server 216 connection.sendNonza(new AuthMechanism(getName(), authenticationText)); 217 } 218 219 /** 220 * Should return the initial response of the SASL mechanism. The returned byte array will be 221 * send base64 encoded to the server. SASL mechanism are free to return <code>null</code> or an 222 * empty array here. 223 * 224 * @return the initial response or null 225 * @throws SmackSaslException if a SASL specific error occurred. 226 */ 227 protected abstract byte[] getAuthenticationText() throws SmackSaslException; 228 229 /** 230 * The server is challenging the SASL mechanism for the stanza he just sent. Send a 231 * response to the server's challenge. 232 * 233 * @param challengeString a base64 encoded string representing the challenge. 234 * @param finalChallenge true if this is the last challenge send by the server within the success stanza 235 * @throws SmackSaslException if a SASL related error occurs. 236 * @throws InterruptedException if the connection is interrupted 237 * @throws NotConnectedException if the XMPP connection is not connected. 238 */ 239 public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackSaslException, InterruptedException, NotConnectedException { 240 byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString); 241 byte[] response = evaluateChallenge(challenge); 242 if (finalChallenge) { 243 return; 244 } 245 246 Response responseStanza; 247 if (response == null) { 248 responseStanza = new Response(); 249 } 250 else { 251 responseStanza = new Response(Base64.encodeToString(response)); 252 } 253 254 // Send the authentication to the server 255 connection.sendNonza(responseStanza); 256 } 257 258 /** 259 * Evaluate the SASL challenge. 260 * 261 * @param challenge challenge to evaluate. 262 * 263 * @return null. 264 * @throws SmackSaslException If a SASL related error occurs. 265 */ 266 protected byte[] evaluateChallenge(byte[] challenge) throws SmackSaslException { 267 return null; 268 } 269 270 @Override 271 public final int compareTo(SASLMechanism other) { 272 Integer ourPriority = getPriority(); 273 return Integer.compare(ourPriority, other.getPriority()); 274 } 275 276 /** 277 * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI. 278 * 279 * @return the common name of the SASL mechanism. 280 */ 281 public abstract String getName(); 282 283 /** 284 * Get the priority of this SASL mechanism. Lower values mean higher priority. 285 * 286 * @return the priority of this SASL mechanism. 287 */ 288 public abstract int getPriority(); 289 290 /** 291 * Check if the SASL mechanism was successful and if it was, then mark it so. 292 * 293 * @throws SmackSaslException in case of an SASL error. 294 */ 295 public final void afterFinalSaslChallenge() throws SmackSaslException { 296 checkIfSuccessfulOrThrow(); 297 298 authenticationSuccessful = true; 299 } 300 301 protected abstract void checkIfSuccessfulOrThrow() throws SmackSaslException; 302 303 public SASLMechanism instanceForAuthentication(XMPPConnection connection, ConnectionConfiguration connectionConfiguration) { 304 SASLMechanism saslMechansim = newInstance(); 305 saslMechansim.connection = connection; 306 saslMechansim.connectionConfiguration = connectionConfiguration; 307 return saslMechansim; 308 } 309 310 public boolean authzidSupported() { 311 return false; 312 } 313 314 public boolean requiresPassword() { 315 return true; 316 } 317 318 public boolean isAuthenticationSuccessful() { 319 return authenticationSuccessful; 320 } 321 322 public boolean isFinished() { 323 return isAuthenticationSuccessful() || exception != null; 324 } 325 326 public void throwExceptionIfRequired() throws SmackSaslException, SASLErrorException, NotConnectedException, 327 InterruptedException, NoResponseException { 328 if (exception != null) { 329 if (exception instanceof SmackSaslException) { 330 throw (SmackSaslException) exception; 331 } else if (exception instanceof SASLErrorException) { 332 throw (SASLErrorException) exception; 333 } else if (exception instanceof NotConnectedException) { 334 throw (NotConnectedException) exception; 335 } else if (exception instanceof InterruptedException) { 336 throw (InterruptedException) exception; 337 } else { 338 throw new IllegalStateException("Unexpected exception type", exception); 339 } 340 } 341 342 if (!authenticationSuccessful) { 343 throw NoResponseException.newWith(connection, "successful SASL authentication"); 344 } 345 } 346 347 public void setException(Exception exception) { 348 this.exception = exception; 349 } 350 351 protected abstract SASLMechanism newInstance(); 352 353 protected static byte[] toBytes(String string) { 354 return StringUtils.toUtf8Bytes(string); 355 } 356 357 /** 358 * SASLprep the given String. The resulting String is in UTF-8. 359 * 360 * @param string the String to sasl prep. 361 * @return the given String SASL preped 362 * @see <a href="http://tools.ietf.org/html/rfc4013">RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords</a> 363 */ 364 protected static String saslPrep(String string) { 365 return Normalizer.normalize(string, Form.NFKC); 366 } 367 368 @Override 369 public final String toString() { 370 return "SASL Mech: " + getName() + ", Prio: " + getPriority(); 371 } 372}