1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-06-29 06:44:50 +02:00
Smack/core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java
Florian Schmaus fcc8414a92 "not connected" is now a checked Exception thrown by sendPacket()
There is a unsolveable race condition between the connection state and
sendPacket(), i.e. the connection could go down, right after the
method calling sendPacket is called, but before sendPacket() is
invoked. Before this change, sendPacket() has thrown an unchecked
IllegalStateException, which could be ignored by the Smack user, who
would also not notice the race condition. We have decided to throw a
checked Exception in this case now, to make the Smack user aware of
this situation.

SMACK-426
2014-03-19 15:56:41 +01:00

393 lines
15 KiB
Java

/**
*
* Copyright 2003-2007 Jive Software.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.StringUtils;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.sasl.RealmCallback;
import javax.security.sasl.RealmChoiceCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
/**
* Base class for SASL mechanisms. Subclasses must implement these methods:
* <ul>
* <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
* </ul>
* Subclasses will likely want to implement their own versions of these methods:
* <li>{@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the
* deprecated method.</li>
* <li>{@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza
* using the CallbackHandler method.</li>
* <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
* </ul>
*
* Basic XMPP SASL authentication steps:
* 1. Client authentication initialization, stanza sent to the server (Base64 encoded):
* <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
* 2. Server sends back to the client the challenge response (Base64 encoded)
* sample:
* realm=<sasl server realm>,nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8,algorithm=md5-sess
* 3. The client responds back to the server (Base 64 encoded):
* sample:
* username=<userid>,realm=<sasl server realm from above>,nonce="OA6MG9tEQGm2hh",
* cnonce="OA6MHXh6VqTrRk",nc=00000001,qop=auth,
* digest-uri=<digesturi>,
* response=d388dad90d4bbd760a152321f2143af7,
* charset=utf-8,
* authzid=<id>
* 4. The server evaluates if the user is present and contained in the REALM
* if successful it sends: <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> (Base64 encoded)
* if not successful it sends:
* sample:
* <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
* cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==
* </challenge>
*
* @author Jay Kline
*/
public abstract class SASLMechanism implements CallbackHandler {
private SASLAuthentication saslAuthentication;
protected SaslClient sc;
protected String authenticationId;
protected String password;
protected String hostname;
public SASLMechanism(SASLAuthentication saslAuthentication) {
this.saslAuthentication = saslAuthentication;
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of
* authentication is not recommended, since it is very inflexable. Use
* {@link #authenticate(String, CallbackHandler)} whenever possible.
*
* Explanation of auth stanza:
*
* The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName
* From RFC-2831:
* digest-uri = "digest-uri" "=" digest-uri-value
* digest-uri-value = serv-type "/" host [ "/" serv-name ]
*
* digest-uri:
* Indicates the principal name of the service with which the client
* wishes to connect, formed from the serv-type, host, and serv-name.
* For example, the FTP service
* on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP
* server from the example above would have a "digest-uri" value of
* "smtp/mail3.example.com/example.com".
*
* host:
* The DNS host name or IP address for the service requested. The DNS host name
* must be the fully-qualified canonical name of the host. The DNS host name is the
* preferred form; see notes on server processing of the digest-uri.
*
* serv-name:
* Indicates the name of the service if it is replicated. The service is
* considered to be replicated if the client's service-location process involves resolution
* using standard DNS lookup operations, and if these operations involve DNS records (such
* as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case,
* the initial name used by the client is the "serv-name", and the final name is the "host"
* component. For example, the incoming mail service for "example.com" may be replicated
* through the use of MX records stored in the DNS, one of which points at an SMTP server
* called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be
* "mail3.example.com". If the service is not replicated, or the serv-name is identical to
* the host, then the serv-name component MUST be omitted
*
* digest-uri verification is needed for ejabberd 2.0.3 and higher
*
* @param username the username of the user being authenticated.
* @param host the hostname where the user account resides.
* @param serviceName the xmpp service location - used by the SASL client in digest-uri creation
* serviceName format is: host [ "/" serv-name ] as per RFC-2831
* @param password the password for this account.
* @throws IOException If a network error occurs while authenticating.
* @throws SaslException
* @throws NotConnectedException
*/
public void authenticate(String username, String host, String serviceName, String password) throws IOException, SaslException, NotConnectedException {
//Since we were not provided with a CallbackHandler, we will use our own with the given
//information
//Set the authenticationID as the username, since they must be the same in this case.
this.authenticationId = username;
this.password = password;
this.hostname = host;
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String,String>();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this);
authenticate();
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
* any additional information, such as the authentication ID or realm, if it is needed.
*
* @param host the hostname where the user account resides.
* @param cbh the CallbackHandler to obtain user information.
* @throws IOException If a network error occures while authenticating.
* @throws SaslException If a protocol error occurs or the user is not authenticated.
* @throws NotConnectedException
*/
public void authenticate(String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException {
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String,String>();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
authenticate();
}
protected void authenticate() throws IOException, SaslException, NotConnectedException {
String authenticationText = null;
if (sc.hasInitialResponse()) {
byte[] response = sc.evaluateChallenge(new byte[0]);
authenticationText = StringUtils.encodeBase64(response, false);
}
// Send the authentication to the server
getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));
}
/**
* The server is challenging the SASL mechanism for the stanza he just sent. Send a
* response to the server's challenge.
*
* @param challenge a base64 encoded string representing the challenge.
* @throws IOException if an exception sending the response occurs.
* @throws NotConnectedException
*/
public void challengeReceived(String challenge) throws IOException, NotConnectedException {
byte response[];
if(challenge != null) {
response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge));
} else {
response = sc.evaluateChallenge(new byte[0]);
}
Packet responseStanza;
if (response == null) {
responseStanza = new Response();
}
else {
responseStanza = new Response(StringUtils.encodeBase64(response, false));
}
// Send the authentication to the server
getSASLAuthentication().send(responseStanza);
}
/**
* Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
*
* @return the common name of the SASL mechanism.
*/
protected abstract String getName();
protected SASLAuthentication getSASLAuthentication() {
return saslAuthentication;
}
/**
*
*/
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
NameCallback ncb = (NameCallback)callbacks[i];
ncb.setName(authenticationId);
} else if(callbacks[i] instanceof PasswordCallback) {
PasswordCallback pcb = (PasswordCallback)callbacks[i];
pcb.setPassword(password.toCharArray());
} else if(callbacks[i] instanceof RealmCallback) {
RealmCallback rcb = (RealmCallback)callbacks[i];
//Retrieve the REALM from the challenge response that the server returned when the client initiated the authentication
//exchange. If this value is not null or empty, *this value* has to be sent back to the server in the client's response
//to the server's challenge
String text = rcb.getDefaultText();
//The SASL client (sc) created in smack uses rcb.getText when creating the negotiatedRealm to send it back to the server
//Make sure that this value matches the server's realm
rcb.setText(text);
} else if(callbacks[i] instanceof RealmChoiceCallback){
//unused
//RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
} else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
/**
* Initiating SASL authentication by select a mechanism.
*/
public static class AuthMechanism extends Packet {
final private String name;
final private String authenticationText;
public AuthMechanism(String name, String authenticationText) {
if (name == null) {
throw new NullPointerException("SASL mechanism name shouldn't be null.");
}
this.name = name;
this.authenticationText = authenticationText;
}
public String toXML() {
StringBuilder stanza = new StringBuilder();
stanza.append("<auth mechanism=\"").append(name);
stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null &&
authenticationText.trim().length() > 0) {
stanza.append(authenticationText);
}
stanza.append("</auth>");
return stanza.toString();
}
}
/**
* A SASL challenge stanza.
*/
public static class Challenge extends Packet {
final private String data;
public Challenge(String data) {
this.data = data;
}
public String toXML() {
StringBuilder stanza = new StringBuilder();
stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null &&
data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</challenge>");
return stanza.toString();
}
}
/**
* A SASL response stanza.
*/
public static class Response extends Packet {
final private String authenticationText;
public Response() {
authenticationText = null;
}
public Response(String authenticationText) {
if (authenticationText == null || authenticationText.trim().length() == 0) {
this.authenticationText = null;
}
else {
this.authenticationText = authenticationText;
}
}
public String toXML() {
StringBuilder stanza = new StringBuilder();
stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null) {
stanza.append(authenticationText);
}
stanza.append("</response>");
return stanza.toString();
}
}
/**
* A SASL success stanza.
*/
public static class Success extends Packet {
final private String data;
public Success(String data) {
this.data = data;
}
public String toXML() {
StringBuilder stanza = new StringBuilder();
stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null &&
data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</success>");
return stanza.toString();
}
}
/**
* A SASL failure stanza.
*/
public static class SASLFailure extends Packet {
private final SASLError saslError;
private final String saslErrorString;
public SASLFailure(String saslError) {
SASLError error = SASLError.fromString(saslError);
if (error == null) {
// RFC6120 6.5 states that unknown condition must be treat as generic authentication failure.
this.saslError = SASLError.not_authorized;
} else {
this.saslError = error;
}
this.saslErrorString = saslError;
}
/**
* Get the SASL related error condition.
*
* @return the SASL related error condition.
*/
public SASLError getSASLError() {
return saslError;
}
/**
*
* @return the SASL error as String
*/
public String getSASLErrorString() {
return saslErrorString;
}
public String toXML() {
StringBuilder stanza = new StringBuilder();
stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
stanza.append("<").append(saslErrorString).append("/>");
stanza.append("</failure>");
return stanza.toString();
}
}
}