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 */ 017package org.jivesoftware.smack.sasl; 018 019import org.jivesoftware.smack.SASLAuthentication; 020import org.jivesoftware.smack.SmackException.NotConnectedException; 021import org.jivesoftware.smack.packet.Packet; 022import org.jivesoftware.smack.util.StringUtils; 023 024import java.io.IOException; 025import java.util.Map; 026import java.util.HashMap; 027 028import javax.security.auth.callback.CallbackHandler; 029import javax.security.auth.callback.UnsupportedCallbackException; 030import javax.security.auth.callback.Callback; 031import javax.security.auth.callback.NameCallback; 032import javax.security.auth.callback.PasswordCallback; 033import javax.security.sasl.RealmCallback; 034import javax.security.sasl.RealmChoiceCallback; 035import javax.security.sasl.Sasl; 036import javax.security.sasl.SaslClient; 037import javax.security.sasl.SaslException; 038 039/** 040 * Base class for SASL mechanisms. Subclasses must implement these methods: 041 * <ul> 042 * <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li> 043 * </ul> 044 * Subclasses will likely want to implement their own versions of these methods: 045 * <li>{@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the 046 * deprecated method.</li> 047 * <li>{@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza 048 * using the CallbackHandler method.</li> 049 * <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li> 050 * </ul> 051 * 052 * Basic XMPP SASL authentication steps: 053 * 1. Client authentication initialization, stanza sent to the server (Base64 encoded): 054 * <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/> 055 * 2. Server sends back to the client the challenge response (Base64 encoded) 056 * sample: 057 * realm=<sasl server realm>,nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8,algorithm=md5-sess 058 * 3. The client responds back to the server (Base 64 encoded): 059 * sample: 060 * username=<userid>,realm=<sasl server realm from above>,nonce="OA6MG9tEQGm2hh", 061 * cnonce="OA6MHXh6VqTrRk",nc=00000001,qop=auth, 062 * digest-uri=<digesturi>, 063 * response=d388dad90d4bbd760a152321f2143af7, 064 * charset=utf-8, 065 * authzid=<id> 066 * 4. The server evaluates if the user is present and contained in the REALM 067 * if successful it sends: <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> (Base64 encoded) 068 * if not successful it sends: 069 * sample: 070 * <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> 071 * cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== 072 * </challenge> 073 * 074 * @author Jay Kline 075 */ 076public abstract class SASLMechanism implements CallbackHandler { 077 078 private SASLAuthentication saslAuthentication; 079 protected SaslClient sc; 080 protected String authenticationId; 081 protected String password; 082 protected String hostname; 083 084 public SASLMechanism(SASLAuthentication saslAuthentication) { 085 this.saslAuthentication = saslAuthentication; 086 } 087 088 /** 089 * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of 090 * authentication is not recommended, since it is very inflexable. Use 091 * {@link #authenticate(String, CallbackHandler)} whenever possible. 092 * 093 * Explanation of auth stanza: 094 * 095 * The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName 096 * From RFC-2831: 097 * digest-uri = "digest-uri" "=" digest-uri-value 098 * digest-uri-value = serv-type "/" host [ "/" serv-name ] 099 * 100 * digest-uri: 101 * Indicates the principal name of the service with which the client 102 * wishes to connect, formed from the serv-type, host, and serv-name. 103 * For example, the FTP service 104 * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP 105 * server from the example above would have a "digest-uri" value of 106 * "smtp/mail3.example.com/example.com". 107 * 108 * host: 109 * The DNS host name or IP address for the service requested. The DNS host name 110 * must be the fully-qualified canonical name of the host. The DNS host name is the 111 * preferred form; see notes on server processing of the digest-uri. 112 * 113 * serv-name: 114 * Indicates the name of the service if it is replicated. The service is 115 * considered to be replicated if the client's service-location process involves resolution 116 * using standard DNS lookup operations, and if these operations involve DNS records (such 117 * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case, 118 * the initial name used by the client is the "serv-name", and the final name is the "host" 119 * component. For example, the incoming mail service for "example.com" may be replicated 120 * through the use of MX records stored in the DNS, one of which points at an SMTP server 121 * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be 122 * "mail3.example.com". If the service is not replicated, or the serv-name is identical to 123 * the host, then the serv-name component MUST be omitted 124 * 125 * digest-uri verification is needed for ejabberd 2.0.3 and higher 126 * 127 * @param username the username of the user being authenticated. 128 * @param host the hostname where the user account resides. 129 * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation 130 * serviceName format is: host [ "/" serv-name ] as per RFC-2831 131 * @param password the password for this account. 132 * @throws IOException If a network error occurs while authenticating. 133 * @throws SaslException 134 * @throws NotConnectedException 135 */ 136 public void authenticate(String username, String host, String serviceName, String password) throws IOException, SaslException, NotConnectedException { 137 //Since we were not provided with a CallbackHandler, we will use our own with the given 138 //information 139 140 //Set the authenticationID as the username, since they must be the same in this case. 141 this.authenticationId = username; 142 this.password = password; 143 this.hostname = host; 144 145 String[] mechanisms = { getName() }; 146 Map<String,String> props = new HashMap<String,String>(); 147 sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this); 148 authenticate(); 149 } 150 151 /** 152 * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle 153 * any additional information, such as the authentication ID or realm, if it is needed. 154 * 155 * @param host the hostname where the user account resides. 156 * @param cbh the CallbackHandler to obtain user information. 157 * @throws IOException If a network error occures while authenticating. 158 * @throws SaslException If a protocol error occurs or the user is not authenticated. 159 * @throws NotConnectedException 160 */ 161 public void authenticate(String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException { 162 String[] mechanisms = { getName() }; 163 Map<String,String> props = new HashMap<String,String>(); 164 sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh); 165 authenticate(); 166 } 167 168 protected void authenticate() throws IOException, SaslException, NotConnectedException { 169 String authenticationText = null; 170 if (sc.hasInitialResponse()) { 171 byte[] response = sc.evaluateChallenge(new byte[0]); 172 authenticationText = StringUtils.encodeBase64(response, false); 173 } 174 175 // Send the authentication to the server 176 getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText)); 177 } 178 179 /** 180 * The server is challenging the SASL mechanism for the stanza he just sent. Send a 181 * response to the server's challenge. 182 * 183 * @param challenge a base64 encoded string representing the challenge. 184 * @throws IOException if an exception sending the response occurs. 185 * @throws NotConnectedException 186 */ 187 public void challengeReceived(String challenge) throws IOException, NotConnectedException { 188 byte response[]; 189 if(challenge != null) { 190 response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge)); 191 } else { 192 response = sc.evaluateChallenge(new byte[0]); 193 } 194 195 Packet responseStanza; 196 if (response == null) { 197 responseStanza = new Response(); 198 } 199 else { 200 responseStanza = new Response(StringUtils.encodeBase64(response, false)); 201 } 202 203 // Send the authentication to the server 204 getSASLAuthentication().send(responseStanza); 205 } 206 207 /** 208 * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI. 209 * 210 * @return the common name of the SASL mechanism. 211 */ 212 protected abstract String getName(); 213 214 protected SASLAuthentication getSASLAuthentication() { 215 return saslAuthentication; 216 } 217 218 /** 219 * 220 */ 221 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { 222 for (int i = 0; i < callbacks.length; i++) { 223 if (callbacks[i] instanceof NameCallback) { 224 NameCallback ncb = (NameCallback)callbacks[i]; 225 ncb.setName(authenticationId); 226 } else if(callbacks[i] instanceof PasswordCallback) { 227 PasswordCallback pcb = (PasswordCallback)callbacks[i]; 228 pcb.setPassword(password.toCharArray()); 229 } else if(callbacks[i] instanceof RealmCallback) { 230 RealmCallback rcb = (RealmCallback)callbacks[i]; 231 //Retrieve the REALM from the challenge response that the server returned when the client initiated the authentication 232 //exchange. If this value is not null or empty, *this value* has to be sent back to the server in the client's response 233 //to the server's challenge 234 String text = rcb.getDefaultText(); 235 //The SASL client (sc) created in smack uses rcb.getText when creating the negotiatedRealm to send it back to the server 236 //Make sure that this value matches the server's realm 237 rcb.setText(text); 238 } else if(callbacks[i] instanceof RealmChoiceCallback){ 239 //unused 240 //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i]; 241 } else { 242 throw new UnsupportedCallbackException(callbacks[i]); 243 } 244 } 245 } 246 247 /** 248 * Initiating SASL authentication by select a mechanism. 249 */ 250 public static class AuthMechanism extends Packet { 251 final private String name; 252 final private String authenticationText; 253 254 public AuthMechanism(String name, String authenticationText) { 255 if (name == null) { 256 throw new NullPointerException("SASL mechanism name shouldn't be null."); 257 } 258 this.name = name; 259 this.authenticationText = authenticationText; 260 } 261 262 public String toXML() { 263 StringBuilder stanza = new StringBuilder(); 264 stanza.append("<auth mechanism=\"").append(name); 265 stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); 266 if (authenticationText != null && 267 authenticationText.trim().length() > 0) { 268 stanza.append(authenticationText); 269 } 270 stanza.append("</auth>"); 271 return stanza.toString(); 272 } 273 } 274 275 /** 276 * A SASL challenge stanza. 277 */ 278 public static class Challenge extends Packet { 279 final private String data; 280 281 public Challenge(String data) { 282 this.data = data; 283 } 284 285 public String toXML() { 286 StringBuilder stanza = new StringBuilder(); 287 stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); 288 if (data != null && 289 data.trim().length() > 0) { 290 stanza.append(data); 291 } 292 stanza.append("</challenge>"); 293 return stanza.toString(); 294 } 295 } 296 297 /** 298 * A SASL response stanza. 299 */ 300 public static class Response extends Packet { 301 final private String authenticationText; 302 303 public Response() { 304 authenticationText = null; 305 } 306 307 public Response(String authenticationText) { 308 if (authenticationText == null || authenticationText.trim().length() == 0) { 309 this.authenticationText = null; 310 } 311 else { 312 this.authenticationText = authenticationText; 313 } 314 } 315 316 public String toXML() { 317 StringBuilder stanza = new StringBuilder(); 318 stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); 319 if (authenticationText != null) { 320 stanza.append(authenticationText); 321 } 322 stanza.append("</response>"); 323 return stanza.toString(); 324 } 325 } 326 327 /** 328 * A SASL success stanza. 329 */ 330 public static class Success extends Packet { 331 final private String data; 332 333 public Success(String data) { 334 this.data = data; 335 } 336 337 public String toXML() { 338 StringBuilder stanza = new StringBuilder(); 339 stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); 340 if (data != null && 341 data.trim().length() > 0) { 342 stanza.append(data); 343 } 344 stanza.append("</success>"); 345 return stanza.toString(); 346 } 347 } 348 349 /** 350 * A SASL failure stanza. 351 */ 352 public static class SASLFailure extends Packet { 353 private final SASLError saslError; 354 private final String saslErrorString; 355 356 public SASLFailure(String saslError) { 357 SASLError error = SASLError.fromString(saslError); 358 if (error == null) { 359 // RFC6120 6.5 states that unknown condition must be treat as generic authentication failure. 360 this.saslError = SASLError.not_authorized; 361 } else { 362 this.saslError = error; 363 } 364 this.saslErrorString = saslError; 365 } 366 367 /** 368 * Get the SASL related error condition. 369 * 370 * @return the SASL related error condition. 371 */ 372 public SASLError getSASLError() { 373 return saslError; 374 } 375 376 /** 377 * 378 * @return the SASL error as String 379 */ 380 public String getSASLErrorString() { 381 return saslErrorString; 382 } 383 384 public String toXML() { 385 StringBuilder stanza = new StringBuilder(); 386 stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); 387 stanza.append("<").append(saslErrorString).append("/>"); 388 stanza.append("</failure>"); 389 return stanza.toString(); 390 } 391 } 392}