Commit 63c7d541 authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

1) Big refactoring work. JM-678

2) sucess stanzas may include final challenge info. JM-679

git-svn-id: http://svn.igniterealtime.org/svn/repos/wildfire/trunk@3874 b35dd754-fafc-0310-a699-88a17e54d16e
parent d547c1f4
...@@ -11,9 +11,7 @@ ...@@ -11,9 +11,7 @@
package org.jivesoftware.wildfire.net; package org.jivesoftware.wildfire.net;
import org.dom4j.DocumentException;
import org.dom4j.Element; import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log; import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils; import org.jivesoftware.util.StringUtils;
...@@ -25,13 +23,13 @@ import org.jivesoftware.wildfire.auth.AuthToken; ...@@ -25,13 +23,13 @@ import org.jivesoftware.wildfire.auth.AuthToken;
import org.jivesoftware.wildfire.auth.UnauthorizedException; import org.jivesoftware.wildfire.auth.UnauthorizedException;
import org.jivesoftware.wildfire.server.IncomingServerSession; import org.jivesoftware.wildfire.server.IncomingServerSession;
import org.jivesoftware.wildfire.user.UserManager; import org.jivesoftware.wildfire.user.UserManager;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.security.sasl.Sasl; import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException; import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer; import javax.security.sasl.SaslServer;
import java.io.IOException; import java.io.UnsupportedEncodingException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
...@@ -85,18 +83,21 @@ public class SASLAuthentication { ...@@ -85,18 +83,21 @@ public class SASLAuthentication {
} }
} }
private SocketConnection connection; public enum Status {
private Session session;
private XMPPPacketReader reader;
/** /**
* * Entity needs to respond last challenge. Session is still negotiating
* SASL authentication.
*/
needResponse,
/**
* SASL negotiation has failed. The entity may retry a few times before the connection
* is closed.
*/ */
public SASLAuthentication(Session session, XMPPPacketReader reader) { failed,
this.session = session; /**
this.connection = (SocketConnection) session.getConnection(); * SASL negotiation has been successful.
this.reader = reader; */
authenticated
} }
/** /**
...@@ -140,47 +141,43 @@ public class SASLAuthentication { ...@@ -140,47 +141,43 @@ public class SASLAuthentication {
return sb.toString(); return sb.toString();
} }
// Do the SASL handshake /**
public boolean doHandshake(Element doc) * Handles the SASL authentication packet. The entity may be sending an initial
throws IOException, DocumentException, XmlPullParserException { * authentication request or a response to a challenge made by the server. The returned
boolean isComplete = false; * value indicates whether the authentication has finished either successfully or not or
boolean success = false; * if the entity is expected to send a response to a challenge.
*
while (!isComplete) { * @param session the session that is authenticating with the server.
* @param doc the stanza sent by the authenticating entity.
* @return value that indicates whether the authentication has finished either successfully
* or not or if the entity is expected to send a response to a challenge.
* @throws UnsupportedEncodingException If UTF-8 charset is not supported.
*/
public static Status handle(Session session, Element doc) throws UnsupportedEncodingException {
Status status;
String mechanism;
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) { if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName()); ElementType type = ElementType.valueof(doc.getName());
switch (type) { switch (type) {
case AUTH: case AUTH:
String mechanism = doc.attributeValue("mechanism"); mechanism = doc.attributeValue("mechanism");
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism); //Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
if (mechanism.equalsIgnoreCase("PLAIN")) { if (mechanism.equalsIgnoreCase("PLAIN") &&
if (getSupportedMechanisms().contains("PLAIN")) { getSupportedMechanisms().contains("PLAIN")) {
success = doPlainAuthentication(doc); status = doPlainAuthentication(session, doc);
}
else {
// TODO Send auth failed before closing connection
success = false;
}
isComplete = true;
} }
else if (mechanism.equalsIgnoreCase("ANONYMOUS")) { else if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
if (getSupportedMechanisms().contains("ANONYMOUS")) { getSupportedMechanisms().contains("ANONYMOUS")) {
success = doAnonymousAuthentication(); status = doAnonymousAuthentication(session);
}
else {
// TODO Send auth failed before closing connection
success = false;
}
isComplete = true;
} }
else if (mechanism.equalsIgnoreCase("EXTERNAL")) { else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
success = doExternalAuthentication(doc); status = doExternalAuthentication(session, doc);
isComplete = true;
} }
else { else if (getSupportedMechanisms().contains(mechanism)) {
// The selected SASL mechanism requires the server to send a challenge // The selected SASL mechanism requires the server to send a challenge
// to the client // to the client
if (getSupportedMechanisms().contains(mechanism)) {
try { try {
Map<String, String> props = new TreeMap<String, String>(); Map<String, String> props = new TreeMap<String, String>();
props.put(Sasl.QOP, "auth"); props.put(Sasl.QOP, "auth");
...@@ -201,35 +198,44 @@ public class SASLAuthentication { ...@@ -201,35 +198,44 @@ public class SASLAuthentication {
} }
byte[] challenge = ss.evaluateResponse(token); byte[] challenge = ss.evaluateResponse(token);
// Send the challenge // Send the challenge
sendChallenge(challenge); sendChallenge(session, challenge);
session.setSessionData("SaslServer", ss); session.setSessionData("SaslServer", ss);
status = Status.needResponse;
} }
catch (SaslException e) { catch (SaslException e) {
isComplete = true;
Log.warn("SaslException", e); Log.warn("SaslException", e);
authenticationFailed(); authenticationFailed(session);
status = Status.failed;
} }
} }
else { else {
// TODO Send auth failed before closing connection
Log.warn("Client wants to do a MECH we don't support: '" + Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'"); mechanism + "'");
isComplete = true; authenticationFailed(session);
success = false; status = Status.failed;
}
} }
break; break;
case RESPONSE: case RESPONSE:
// Store the requested SASL mechanism by the client
mechanism = (String) session.getSessionData("SaslMechanism");
if (mechanism.equalsIgnoreCase("PLAIN") &&
getSupportedMechanisms().contains("PLAIN")) {
status = doPlainAuthentication(session, doc);
}
else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (getSupportedMechanisms().contains(mechanism)) {
SaslServer ss = (SaslServer) session.getSessionData("SaslServer"); SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
if (ss != null) { if (ss != null) {
boolean ssComplete = ss.isComplete(); boolean ssComplete = ss.isComplete();
String response = doc.getTextTrim(); String response = doc.getTextTrim();
try { try {
if (ssComplete) { if (ssComplete) {
authenticationSuccessful(ss.getAuthorizationID(), null); authenticationSuccessful(session, ss.getAuthorizationID(),
success = true; null);
isComplete = true; status = Status.authenticated;
} }
else { else {
byte[] data = StringUtils.decodeBase64(response); byte[] data = StringUtils.decodeBase64(response);
...@@ -238,82 +244,82 @@ public class SASLAuthentication { ...@@ -238,82 +244,82 @@ public class SASLAuthentication {
} }
byte[] challenge = ss.evaluateResponse(data); byte[] challenge = ss.evaluateResponse(data);
if (ss.isComplete()) { if (ss.isComplete()) {
authenticationSuccessful(ss.getAuthorizationID(), authenticationSuccessful(session, ss.getAuthorizationID(),
challenge); challenge);
success = true; status = Status.authenticated;
isComplete = true;
} }
else { else {
// Send the challenge // Send the challenge
sendChallenge(challenge); sendChallenge(session, challenge);
status = Status.needResponse;
} }
} }
} }
catch (SaslException e) { catch (SaslException e) {
isComplete = true;
Log.warn("SaslException", e); Log.warn("SaslException", e);
authenticationFailed(); authenticationFailed(session);
status = Status.failed;
} }
} }
else { else {
isComplete = true;
Log.fatal("SaslServer is null, should be valid object instead."); Log.fatal("SaslServer is null, should be valid object instead.");
authenticationFailed(); authenticationFailed(session);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session);
status = Status.failed;
} }
break; break;
default: default:
authenticationFailed(session);
status = Status.failed;
// Ignore // Ignore
break; break;
} }
if (!isComplete) {
// Get the next answer since we are not done yet
doc = reader.parseDocument().getRootElement();
if (doc == null) {
// Nothing was read because the connection was closed or dropped
isComplete = true;
}
}
} }
else { else {
isComplete = true;
Log.debug("Unknown namespace sent in auth element: " + doc.asXML()); Log.debug("Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(); authenticationFailed(session);
} status = Status.failed;
} }
// Check if SASL authentication has finished so we can clean up temp information
if (status == Status.failed || status == Status.authenticated) {
// Remove the SaslServer from the Session // Remove the SaslServer from the Session
session.removeSessionData("SaslServer"); session.removeSessionData("SaslServer");
return success; // Remove the requested SASL mechanism by the client
session.removeSessionData("SaslMechanism");
}
return status;
} }
private boolean doAnonymousAuthentication() { private static Status doAnonymousAuthentication(Session session) {
if (XMPPServer.getInstance().getIQAuthHandler().isAllowAnonymous()) { if (XMPPServer.getInstance().getIQAuthHandler().isAllowAnonymous()) {
// Just accept the authentication :) // Just accept the authentication :)
authenticationSuccessful(null, null); authenticationSuccessful(session, null, null);
return true; return Status.authenticated;
} }
else { else {
// anonymous login is disabled so close the connection // anonymous login is disabled so close the connection
authenticationFailed(); authenticationFailed(session);
return false; return Status.failed;
} }
} }
private boolean doPlainAuthentication(Element doc) private static Status doPlainAuthentication(Session session, Element doc)
throws DocumentException, IOException, XmlPullParserException { throws UnsupportedEncodingException {
String username = ""; String username;
String password = ""; String password;
String response = doc.getTextTrim(); String response = doc.getTextTrim();
if (response == null || response.length() == 0) { if (response == null || response.length() == 0) {
// No info was provided so send a challenge to get it // No info was provided so send a challenge to get it
sendChallenge(new byte[0]); sendChallenge(session, new byte[0]);
// Get the next answer since we are not done yet return Status.needResponse;
doc = reader.parseDocument().getRootElement();
if (doc != null && doc.getTextTrim().length() > 0) {
response = doc.getTextTrim();
}
} }
if (response != null && response.length() > 0) {
// Parse data and obtain username & password // Parse data and obtain username & password
String data = new String(StringUtils.decodeBase64(response), CHARSET); String data = new String(StringUtils.decodeBase64(response), CHARSET);
StringTokenizer tokens = new StringTokenizer(data, "\0"); StringTokenizer tokens = new StringTokenizer(data, "\0");
...@@ -323,63 +329,60 @@ public class SASLAuthentication { ...@@ -323,63 +329,60 @@ public class SASLAuthentication {
} }
username = tokens.nextToken(); username = tokens.nextToken();
password = tokens.nextToken(); password = tokens.nextToken();
}
try { try {
AuthToken token = AuthFactory.authenticate(username, password); AuthToken token = AuthFactory.authenticate(username, password);
authenticationSuccessful(token.getUsername(), null); authenticationSuccessful(session, token.getUsername(), null);
return true; return Status.authenticated;
} }
catch (UnauthorizedException e) { catch (UnauthorizedException e) {
authenticationFailed(); authenticationFailed(session);
return false; return Status.failed;
} }
} }
private boolean doExternalAuthentication(Element doc) throws DocumentException, IOException, private static Status doExternalAuthentication(Session session, Element doc)
XmlPullParserException { throws UnsupportedEncodingException {
// Only accept EXTERNAL SASL for s2s. At this point the connection has already // Only accept EXTERNAL SASL for s2s. At this point the connection has already
// been secured using TLS // been secured using TLS
if (!(session instanceof IncomingServerSession)) { if (!(session instanceof IncomingServerSession)) {
return false; return Status.failed;
} }
String hostname = doc.getTextTrim(); String hostname = doc.getTextTrim();
if (hostname == null || hostname.length() == 0) { if (hostname == null || hostname.length() == 0) {
// No hostname was provided so send a challenge to get it // No hostname was provided so send a challenge to get it
sendChallenge(new byte[0]); sendChallenge(session, new byte[0]);
// Get the next answer since we are not done yet return Status.needResponse;
doc = reader.parseDocument().getRootElement();
if (doc != null && doc.getTextTrim().length() > 0) {
hostname = doc.getTextTrim();
}
} }
if (hostname != null && hostname.length() > 0) {
hostname = new String(StringUtils.decodeBase64(hostname), CHARSET); hostname = new String(StringUtils.decodeBase64(hostname), CHARSET);
// Check if cerificate validation is disabled for s2s // Check if cerificate validation is disabled for s2s
if (session instanceof IncomingServerSession) {
// Flag that indicates if certificates of the remote server should be validated. // Flag that indicates if certificates of the remote server should be validated.
// Disabling certificate validation is not recommended for production environments. // Disabling certificate validation is not recommended for production environments.
boolean verify = boolean verify =
JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify", true); JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify", true);
if (!verify) { if (!verify) {
authenticationSuccessful(hostname, null); authenticationSuccessful(session, hostname, null);
return true; return Status.authenticated;
}
} }
// Check that hostname matches the one provided in a certificate // Check that hostname matches the one provided in a certificate
SocketConnection connection = (SocketConnection) session.getConnection();
try {
for (Certificate certificate : connection.getSSLSession().getPeerCertificates()) { for (Certificate certificate : connection.getSSLSession().getPeerCertificates()) {
if (TLSStreamHandler.getPeerIdentities((X509Certificate) certificate) if (TLSStreamHandler.getPeerIdentities((X509Certificate) certificate)
.contains(hostname)) { .contains(hostname)) {
authenticationSuccessful(hostname, null); authenticationSuccessful(session, hostname, null);
return true; return Status.authenticated;
}
} }
} }
catch (SSLPeerUnverifiedException e) {
Log.warn("Error retrieving client certificates of: " + session, e);
} }
authenticationFailed(); authenticationFailed(session);
return false; return Status.failed;
} }
private void sendChallenge(byte[] challenge) { private static void sendChallenge(Session session, byte[] challenge) {
StringBuilder reply = new StringBuilder(250); StringBuilder reply = new StringBuilder(250);
if(challenge == null) { if(challenge == null) {
challenge = new byte[0]; challenge = new byte[0];
...@@ -392,10 +395,11 @@ public class SASLAuthentication { ...@@ -392,10 +395,11 @@ public class SASLAuthentication {
"<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); "<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append(challenge_b64); reply.append(challenge_b64);
reply.append("</challenge>"); reply.append("</challenge>");
connection.deliverRawText(reply.toString()); session.getConnection().deliverRawText(reply.toString());
} }
private void authenticationSuccessful(String username, byte[] successData) { private static void authenticationSuccessful(Session session, String username,
byte[] successData) {
StringBuilder reply = new StringBuilder(80); StringBuilder reply = new StringBuilder(80);
reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\""); reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"");
if (successData != null) { if (successData != null) {
...@@ -404,7 +408,7 @@ public class SASLAuthentication { ...@@ -404,7 +408,7 @@ public class SASLAuthentication {
else { else {
reply.append("/>"); reply.append("/>");
} }
connection.deliverRawText(reply.toString()); session.getConnection().deliverRawText(reply.toString());
// We only support SASL for c2s // We only support SASL for c2s
if (session instanceof ClientSession) { if (session instanceof ClientSession) {
((ClientSession) session).setAuthToken(new AuthToken(username)); ((ClientSession) session).setAuthToken(new AuthToken(username));
...@@ -419,11 +423,11 @@ public class SASLAuthentication { ...@@ -419,11 +423,11 @@ public class SASLAuthentication {
} }
} }
private void authenticationFailed() { private static void authenticationFailed(Session session) {
StringBuilder reply = new StringBuilder(80); StringBuilder reply = new StringBuilder(80);
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append("<not-authorized/></failure>"); reply.append("<not-authorized/></failure>");
connection.deliverRawText(reply.toString()); session.getConnection().deliverRawText(reply.toString());
// Give a number of retries before closing the connection // Give a number of retries before closing the connection
Integer retries = (Integer) session.getSessionData("authRetries"); Integer retries = (Integer) session.getSessionData("authRetries");
if (retries == null) { if (retries == null) {
...@@ -435,7 +439,7 @@ public class SASLAuthentication { ...@@ -435,7 +439,7 @@ public class SASLAuthentication {
session.setSessionData("authRetries", retries); session.setSessionData("authRetries", retries);
if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) { if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) {
// Close the connection // Close the connection
connection.close(); session.getConnection().close();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment