Commit 6f9f814c authored by Guus der Kinderen's avatar Guus der Kinderen

Merge pull request #548 from guusdk/OF-1092

OF-1092: Pluggable SASL mechanism
parents 4a1ac2bd 9017ee09
......@@ -20,26 +20,6 @@
package org.jivesoftware.openfire.net;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
......@@ -48,16 +28,12 @@ import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.keystore.CertificateStoreManager;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.IncomingServerSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.sasl.Failure;
import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer;
import org.jivesoftware.openfire.sasl.SaslFailureException;
import org.jivesoftware.openfire.session.*;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
......@@ -65,6 +41,16 @@ import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Pattern;
/**
* SASLAuthentication is responsible for returning the available SASL mechanisms to use and for
* actually performing the SASL authentication.<p>
......@@ -89,78 +75,55 @@ public class SASLAuthentication {
// plus an extra regex alternative to catch a single equals sign ('=', see RFC 6120 6.4.2)
private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$");
private static final String SASL_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"";
private static final String SASL_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl";
private static Map<String, ElementType> typeMap = new TreeMap<>();
private static Set<String> mechanisms = new HashSet<>();
private static Set<String> mechanisms = null;
static
{
// Add (proprietary) Providers of SASL implementation to the Java security context.
Security.addProvider( new org.jivesoftware.openfire.sasl.SaslProvider() );
static {
initMechanisms();
}
public enum ElementType {
ABORT("abort"), AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");
private String name = null;
@Override
public String toString() {
return name;
}
private ElementType(String name) {
this.name = name;
typeMap.put(this.name, this);
}
public static ElementType valueof(String name) {
if (name == null) {
public enum ElementType
{
ABORT,
AUTH,
RESPONSE,
CHALLENGE,
FAILURE,
UNDEF;
public static ElementType valueOfCaseInsensitive( String name )
{
if ( name == null || name.isEmpty() ) {
return UNDEF;
}
ElementType e = typeMap.get(name);
return e != null ? e : UNDEF;
}
try
{
return ElementType.valueOf( name.toUpperCase() );
}
private enum Failure {
ABORTED("aborted"),
ACCOUNT_DISABLED("account-disabled"),
CREDENTIALS_EXPIRED("credentials-expired"),
ENCRYPTION_REQUIRED("encryption-required"),
INCORRECT_ENCODING("incorrect-encoding"),
INVALID_AUTHZID("invalid-authzid"),
INVALID_MECHANISM("invalid-mechanism"),
MALFORMED_REQUEST("malformed-request"),
MECHANISM_TOO_WEAK("mechanism-too-weak"),
NOT_AUTHORIZED("not-authorized"),
TEMPORARY_AUTH_FAILURE("temporary-auth-failure");
private String name = null;
private Failure(String name) {
this.name = name;
catch ( Throwable t )
{
return UNDEF;
}
@Override
public String toString() {
return name;
}
}
public enum Status {
public enum Status
{
/**
* Entity needs to respond last challenge. Session is still negotiating
* SASL authentication.
* Entity needs to respond last challenge. Session is still negotiatingSASL authentication.
*/
needResponse,
/**
* SASL negotiation has failed. The entity may retry a few times before the connection
* is closed.
* SASL negotiation has failed. The entity may retry a few times before the connection is closed.
*/
failed,
/**
* SASL negotiation has been successful.
*/
......@@ -176,47 +139,53 @@ public class SASLAuthentication {
*
* @return a string with the valid SASL mechanisms available for the specified session.
*/
public static String getSASLMechanisms(LocalSession session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
public static String getSASLMechanisms( LocalSession session )
{
if ( session instanceof ClientSession )
{
return getSASLMechanismsElement( (ClientSession) session ).asXML();
}
else if ( session instanceof LocalIncomingServerSession )
{
return getSASLMechanismsElement( (LocalIncomingServerSession) session ).asXML();
}
else
{
Log.debug( "Unable to determine SASL mechanisms that are applicable to session '{}'. Unrecognized session type.", session );
return "";
}
Element mechs = getSASLMechanismsElement(session);
return mechs.asXML();
}
public static Element getSASLMechanismsElement(Session session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return null;
public static Element getSASLMechanismsElement( ClientSession session )
{
final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) );
for (String mech : getSupportedMechanisms()) {
final Element mechanism = result.addElement("mechanism");
mechanism.setText(mech);
}
return result;
}
Element mechs = DocumentHelper.createElement( new QName( "mechanisms",
new Namespace( "", "urn:ietf:params:xml:ns:xmpp-sasl" ) ) );
if (session instanceof LocalIncomingServerSession) {
// Server connections don't follow the same rules as clients
public static Element getSASLMechanismsElement( LocalIncomingServerSession session )
{
final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) );
if (session.isSecure()) {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
final KeyStore keyStore = svr.getConnection().getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = svr.getConnection().getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( svr.getConnection().getPeerCertificates(), keyStore, trustStore );
final Connection connection = session.getConnection();
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = session.getConnection().getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( session.getConnection().getPeerCertificates(), keyStore, trustStore );
boolean haveTrustedCertificate = trusted != null;
if (trusted != null && svr.getDefaultIdentity() != null) {
haveTrustedCertificate = verifyCertificate(trusted, svr.getDefaultIdentity());
if (trusted != null && session.getDefaultIdentity() != null) {
haveTrustedCertificate = verifyCertificate(trusted, session.getDefaultIdentity());
}
if (haveTrustedCertificate) {
// Offer SASL EXTERNAL only if TLS has already been negotiated and the peer has a trusted cert.
Element mechanism = mechs.addElement("mechanism");
final Element mechanism = result.addElement("mechanism");
mechanism.setText("EXTERNAL");
}
}
}
else {
for (String mech : getSupportedMechanisms()) {
Element mechanism = mechs.addElement("mechanism");
mechanism.setText(mech);
}
}
return mechs;
return result;
}
/**
......@@ -230,389 +199,141 @@ public class SASLAuthentication {
* @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.
*/
public static Status handle(LocalSession session, Element doc) {
Status status;
String mechanism;
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName());
switch (type) {
case ABORT:
authenticationFailed(session, Failure.ABORTED);
status = Status.failed;
break;
case AUTH:
mechanism = doc.attributeValue("mechanism");
// http://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism
// The initiating entity did not specify a mechanism
if (mechanism == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
break;
}
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
mechanisms.contains("ANONYMOUS")) {
status = doAnonymousAuthentication(session);
}
else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
// The selected SASL mechanism requires the server to send a challenge
// to the client
try {
Map<String, String> props = new TreeMap<>();
props.put(Sasl.QOP, "auth");
if (mechanism.equals("GSSAPI")) {
props.put(Sasl.SERVER_AUTH, "TRUE");
}
SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
session.getServerName(), props,
new XMPPCallbackHandler());
if (ss == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
return Status.failed;
public static Status handle(LocalSession session, Element doc)
{
try
{
if ( !doc.getNamespaceURI().equals( SASL_NAMESPACE ) )
{
throw new IllegalStateException( "Unexpected data received while negotiating SASL authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI() );
}
// evaluateResponse doesn't like null parameter
byte[] token = new byte[0];
String value = doc.getTextTrim();
if (value.length() > 0) {
if (!BASE64_ENCODED.matcher(value).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
// If auth request includes a value then validate it
token = StringUtils.decodeBase64(value);
if (token == null) {
token = new byte[0];
}
}
if (mechanism.equals("DIGEST-MD5")) {
// RFC2831 (DIGEST-MD5) says the client MAY provide an initial response on subsequent
// authentication. Java SASL does not (currently) support this and thows an exception
// if we try. This violates the RFC, so we just strip any initial token.
token = new byte[0];
}
byte[] challenge = ss.evaluateResponse(token);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
session.setSessionData("SaslServer", ss);
}
catch (SaslException e) {
Log.info("User Login Failed. " + e.getMessage());
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
case RESPONSE:
// Store the requested SASL mechanism by the client
mechanism = (String) session.getSessionData("SaslMechanism");
if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanism.equalsIgnoreCase("JIVE-SHAREDSECRET")) {
status = doSharedSecretAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
if (ss != null) {
boolean ssComplete = ss.isComplete();
String response = doc.getTextTrim();
if (response.length() > 0) {
if (!BASE64_ENCODED.matcher(response).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
}
try {
if (ssComplete) {
authenticationSuccessful(session, ss.getAuthorizationID(),
null);
status = Status.authenticated;
}
else {
byte[] data = StringUtils.decodeBase64(response);
if (data == null) {
data = new byte[0];
}
byte[] challenge = ss.evaluateResponse(data);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
}
}
catch (SaslException e) {
Log.debug("SASLAuthentication: SaslException", e);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.error("SaslServer is null, should be valid object instead.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
default:
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
// Ignore
break;
}
}
else {
Log.debug("SASLAuthentication: Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(session, Failure.MALFORMED_REQUEST);
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
session.removeSessionData("SaslServer");
// Remove the requested SASL mechanism by the client
session.removeSessionData("SaslMechanism");
}
return status;
}
switch ( ElementType.valueOfCaseInsensitive( doc.getName() ) )
{
case ABORT:
throw new SaslFailureException( Failure.ABORTED );
/**
* Returns true if shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @return true if shared secret authentication is enabled.
*/
public static boolean isSharedSecretAllowed() {
return JiveGlobals.getBooleanProperty("xmpp.auth.sharedSecretEnabled");
case AUTH:
if ( doc.attributeValue( "mechanism" ) == null )
{
throw new SaslFailureException( Failure.INVALID_MECHANISM, "Peer did not specify a mechanism." );
}
/**
* Sets whether shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @param sharedSecretAllowed true if shared secret authentication should be enabled.
*/
public static void setSharedSecretAllowed(boolean sharedSecretAllowed) {
JiveGlobals.setProperty("xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false");
}
final String mechanismName = doc.attributeValue( "mechanism" ).toUpperCase();
/**
* Returns the shared secret value, or <tt>null</tt> if shared secret authentication is
* disabled. If this is the first time the shared secret value has been requested (and
* shared secret auth is enabled), the key will be randomly generated and stored in the
* property <tt>xmpp.auth.sharedSecret</tt>.
*
* @return the shared secret value.
*/
public static String getSharedSecret() {
if (!isSharedSecretAllowed()) {
return null;
}
String sharedSecret = JiveGlobals.getProperty("xmpp.auth.sharedSecret");
if (sharedSecret == null) {
sharedSecret = StringUtils.randomString(8);
JiveGlobals.setProperty("xmpp.auth.sharedSecret", sharedSecret);
}
return sharedSecret;
// See if the mechanism is supported by configuration as well as by implementation.
if ( !mechanisms.contains( mechanismName ) )
{
throw new SaslFailureException( Failure.INVALID_MECHANISM, "The configuration of Openfire does not contain or allow the mechanism." );
}
/**
* Returns true if the supplied digest matches the shared secret value. The digest
* must be an MD5 hash of the secret key, encoded as hex. This value is supplied
* by clients attempting shared secret authentication.
*
* @param digest the MD5 hash of the secret key, encoded as hex.
* @return true if authentication succeeds.
*/
public static boolean authenticateSharedSecret(String digest) {
if (!isSharedSecretAllowed()) {
return false;
}
String sharedSecert = getSharedSecret();
return StringUtils.hash(sharedSecert).equals( digest );
// Construct the configuration properties
final Map<String, Object> props = new HashMap<>();
props.put( LocalClientSession.class.getCanonicalName(), session );
props.put( Sasl.POLICY_NOANONYMOUS, !XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed() );
SaslServer saslServer = Sasl.createSaslServer( mechanismName, "xmpp", session.getServerName(), props, new XMPPCallbackHandler() );
if ( saslServer == null )
{
throw new SaslFailureException( Failure.INVALID_MECHANISM, "There is no provider that can provide a SASL server for the desired mechanism and properties." );
}
session.setSessionData( "SaslServer", saslServer );
private static Status doAnonymousAuthentication(LocalSession session) {
if (XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
// Verify that client can connect from his IP address
boolean forbidAccess = !LocalClientSession.isAllowedAnonymous( session.getConnection() );
if (forbidAccess) {
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Just accept the authentication :)
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
else {
// anonymous login is disabled so close the connection
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
if ( mechanismName.equals( "DIGEST-MD5" ) )
{
// RFC2831 (DIGEST-MD5) says the client MAY provide data in the initial response. Java SASL does
// not (currently) support this and throws an exception. For XMPP, such data violates
// the RFC, so we just strip any initial token.
doc.setText( "" );
}
private static Status doExternalAuthentication(LocalSession session, Element doc) {
// At this point the connection has already been secured using TLS
// intended fall-through
case RESPONSE:
if (session instanceof IncomingServerSession) {
saslServer = (SaslServer) session.getSessionData( "SaslServer" );
String hostname = doc.getTextTrim();
if (hostname == null || hostname.length() == 0) {
// No hostname was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
if ( saslServer == null )
{
// Client sends response without a preceding auth?
throw new IllegalStateException( "A SaslServer instance was not initialized and/or stored on the session." );
}
hostname = new String(StringUtils.decodeBase64(hostname), StandardCharsets.UTF_8);
if (hostname.length() == 0) {
hostname = null;
}
try {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
String defHostname = svr.getDefaultIdentity();
if (hostname == null) {
hostname = defHostname;
} else if (!hostname.equals(defHostname)) {
// Mismatch; really odd.
Log.info("SASLAuthentication rejected from='{}' and authzid='{}'", hostname, defHostname);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
} catch(Exception e) {
// Erm. Nothing?
}
if (hostname == null) {
Log.info("No authzid supplied for anonymous session.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Check if certificate validation is disabled for s2s
// Flag that indicates if certificates of the remote server should be validated.
// Disabling certificate validation is not recommended for production environments.
boolean verify =
JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true);
if (!verify) {
authenticationSuccessful(session, hostname, null);
return Status.authenticated;
} else if(verifyCertificates(session.getConnection().getPeerCertificates(), hostname, true)) {
authenticationSuccessful(session, hostname, null);
LocalIncomingServerSession s = (LocalIncomingServerSession)session;
if (s != null) {
s.tlsAuth();
}
return Status.authenticated;
// Decode any data that is provided in the client response.
final String encoded = doc.getTextTrim();
final byte[] decoded;
if ( encoded == null || encoded.isEmpty() )
{
decoded = new byte[ 0 ];
}
else
{
// TODO: We shouldn't depend on regex-based validation. Instead, use a proper decoder implementation and handle any exceptions that it throws.
if ( !BASE64_ENCODED.matcher( encoded ).matches() )
{
throw new SaslFailureException( Failure.INCORRECT_ENCODING );
}
else if (session instanceof LocalClientSession) {
// Client EXTERNAL login
Log.debug("SASLAuthentication: EXTERNAL authentication via SSL certs for c2s connection");
// This may be null, we will deal with that later
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), StandardCharsets.UTF_8);
String principal = "";
ArrayList<String> principals = new ArrayList<>();
Connection connection = session.getConnection();
if (connection.getPeerCertificates().length < 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but no certificates found.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
decoded = StringUtils.decodeBase64( encoded );
}
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
// Process client response.
final byte[] challenge = saslServer.evaluateResponse( decoded ); // Either a challenge or success data.
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( connection.getPeerCertificates(), keyStore, trustStore );
if (trusted == null) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but EE cert untrusted.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
if ( !saslServer.isComplete() )
{
// Not complete: client is challenged for additional steps.
sendChallenge( session, challenge );
return Status.needResponse;
}
principals.addAll(CertificateManager.getClientIdentities(trusted));
if(principals.size() == 1) {
principal = principals.get(0);
} else if(principals.size() > 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication: more than one principal found, using first.");
principal = principals.get(0);
} else {
Log.debug("SASLAuthentication: EXTERNAL authentication: No principals found.");
// Success!
if ( session instanceof IncomingServerSession )
{
// Flag that indicates if certificates of the remote server should be validated.
final boolean verify = JiveGlobals.getBooleanProperty( ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true );
if ( verify && verifyCertificates( session.getConnection().getPeerCertificates(), saslServer.getAuthorizationID(), true ) )
{
( (LocalIncomingServerSession) session ).tlsAuth();
}
else
{
throw new SaslFailureException( Failure.NOT_AUTHORIZED, "Server-to-Server certificate verification failed." );
}
}
if (username == null || username.length() == 0) {
// No username was provided, according to XEP-0178 we need to:
// * attempt to get it from the cert first
// * have the server assign one
authenticationSuccessful( session, saslServer.getAuthorizationID(), challenge );
session.removeSessionData( "SaslServer" );
return Status.authenticated;
// There shouldn't be more than a few principals in here. One ideally
// We set principal to the first one in the list to have a sane default
// If this list is empty, then the cert had no identity at all, which
// will cause an authorization failure
for(String princ : principals) {
String u = AuthorizationManager.map(princ);
if(!u.equals(princ)) {
username = u;
principal = princ;
break;
}
default:
throw new IllegalStateException( "Unexpected data received while negotiating SASL authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI() );
}
if (username == null || username.length() == 0) {
// Still no username. Punt.
username = principal;
}
Log.debug("SASLAuthentication: no username requested, using "+username);
catch ( SaslException ex )
{
Log.debug( "SASL negotiation failed for session: {}", session, ex );
final Failure failure;
if ( ex instanceof SaslFailureException && ((SaslFailureException) ex).getFailure() != null )
{
failure = ((SaslFailureException) ex).getFailure();
}
//Its possible that either/both username and principal are null here
//The providers should not allow a null authorization
if (AuthorizationManager.authorize(username,principal)) {
Log.debug("SASLAuthentication: "+principal+" authorized to "+username);
authenticationSuccessful(session, username, null);
return Status.authenticated;
else
{
failure = Failure.NOT_AUTHORIZED;
}
} else {
Log.debug("SASLAuthentication: unknown session type. Cannot perform EXTERNAL authentication");
authenticationFailed( session, failure );
session.removeSessionData( "SaslServer" );
return Status.failed;
}
authenticationFailed(session, Failure.NOT_AUTHORIZED);
catch( Exception ex )
{
Log.warn( "An unexpected exception occurred during SASL negotiation. Affected session: {}", session, ex );
authenticationFailed( session, Failure.NOT_AUTHORIZED );
session.removeSessionData( "SaslServer" );
return Status.failed;
}
}
public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) {
for (String identity : CertificateManager.getServerIdentities(trustedCert)) {
......@@ -640,29 +361,6 @@ public class SASLAuthentication {
return false;
}
private static Status doSharedSecretAuthentication(LocalSession session, Element doc) {
String secretDigest;
String response = doc.getTextTrim();
if (response == null || response.length() == 0) {
// No info was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
// Parse data and obtain username & password
String data = new String(StringUtils.decodeBase64(response), StandardCharsets.UTF_8);
StringTokenizer tokens = new StringTokenizer(data, "\0");
tokens.nextToken();
secretDigest = tokens.nextToken();
if (authenticateSharedSecret(secretDigest)) {
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
// Otherwise, authentication failed.
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
private static void sendChallenge(Session session, byte[] challenge) {
StringBuilder reply = new StringBuilder(250);
if (challenge == null) {
......@@ -739,19 +437,30 @@ public class SASLAuthentication {
* mechanism by Openfire. Actual SASL handling is done by Java itself, so you must add
* the provider to Java.
*
* @param mechanism the new SASL mechanism.
* @param mechanism the name of the new SASL mechanism (cannot be null or an empty String).
*/
public static void addSupportedMechanism(String mechanism) {
mechanisms.add(mechanism);
public static void addSupportedMechanism(String mechanismName) {
if ( mechanismName == null || mechanismName.isEmpty() ) {
throw new IllegalArgumentException( "Argument 'mechanism' must cannot be null or an empty string." );
}
mechanisms.add( mechanismName.toUpperCase() );
Log.info( "Support added for the '{}' SASL mechanism.", mechanismName.toUpperCase() );
}
/**
* Removes a SASL mechanism from the list of supported SASL mechanisms by the server.
*
* @param mechanism the SASL mechanism to remove.
* @param mechanismName the name of the SASL mechanism to remove (cannot be null or empty, not case sensitive).
*/
public static void removeSupportedMechanism(String mechanism) {
mechanisms.remove(mechanism);
public static void removeSupportedMechanism(String mechanismName) {
if ( mechanismName == null || mechanismName.isEmpty() ) {
throw new IllegalArgumentException( "Argument 'mechanism' must cannot be null or an empty string." );
}
if ( mechanisms.remove( mechanismName.toUpperCase() ) )
{
Log.info( "Support removed for the '{}' SASL mechanism.", mechanismName.toUpperCase() );
}
}
/**
......@@ -787,7 +496,7 @@ public class SASLAuthentication {
}
else if (mech.equals("JIVE-SHAREDSECRET")) {
// Check shared secret is supported
if (!isSharedSecretAllowed()) {
if (!JiveSharedSecretSaslServer.isSharedSecretAllowed()) {
it.remove();
}
}
......@@ -795,59 +504,27 @@ public class SASLAuthentication {
return answer;
}
private static void initMechanisms() {
private static void initMechanisms()
{
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("sasl.mechs");
JiveGlobals.migrateProperty("sasl.gssapi.debug");
JiveGlobals.migrateProperty("sasl.gssapi.config");
JiveGlobals.migrateProperty("sasl.gssapi.useSubjectCredsOnly");
mechanisms = new HashSet<>();
String available = JiveGlobals.getProperty("sasl.mechs");
if (available == null) {
mechanisms.add("ANONYMOUS");
mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5");
mechanisms.add("SCRAM-SHA-1");
mechanisms.add("JIVE-SHAREDSECRET");
}
else {
StringTokenizer st = new StringTokenizer(available, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String mech = st.nextToken().toUpperCase();
// Check that the mech is a supported mechansim. Maybe we shouldnt check this and allow any?
if (mech.equals("ANONYMOUS") ||
mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") ||
mech.equals("SCRAM-SHA-1") ||
mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET"))
{
Log.debug("SASLAuthentication: Added " + mech + " to mech list");
mechanisms.add(mech);
}
}
if (mechanisms.contains("GSSAPI")) {
if (JiveGlobals.getProperty("sasl.gssapi.config") != null) {
System.setProperty("java.security.krb5.debug",
JiveGlobals.getProperty("sasl.gssapi.debug", "false"));
System.setProperty("java.security.auth.login.config",
JiveGlobals.getProperty("sasl.gssapi.config"));
System.setProperty("javax.security.auth.useSubjectCredsOnly",
JiveGlobals.getProperty("sasl.gssapi.useSubjectCredsOnly", "false"));
}
else {
//Not configured, remove the option.
Log.debug("SASLAuthentication: Removed GSSAPI from mech list");
mechanisms.remove("GSSAPI");
final String configuration = JiveGlobals.getProperty("sasl.mechs", "ANONYMOUS,PLAIN,DIGEST-MD5,CRAM-MD5,SCRAM-SHA-1,JIVE-SHAREDSECRET" );
final StringTokenizer st = new StringTokenizer(configuration, " ,\t\n\r\f");
while ( st.hasMoreTokens() )
{
final String mech = st.nextToken().toUpperCase();
try
{
addSupportedMechanism( mech );
}
catch ( Exception ex )
{
Log.warn( "An exception occurred while trying to add support for SASL Mechanism '{}':", mech, ex );
}
}
//Add our providers to the Security class
Security.addProvider(new org.jivesoftware.openfire.sasl.SaslProvider());
}
}
package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalSession;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
/**
* Implementation of the SASL ANONYMOUS mechanism.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see <a href="http://tools.ietf.org/html/rfc4505">RFC 4505</a>
* @see <a href="http://xmpp.org/extensions/xep-0175.html">XEP 0175</a>
*/
public class AnonymousSaslServer implements SaslServer
{
public static final String NAME = "ANONYMOUS";
private boolean complete = false;
private LocalSession session;
public AnonymousSaslServer( LocalSession session )
{
this.session = session;
}
@Override
public String getMechanismName()
{
return NAME;
}
@Override
public byte[] evaluateResponse( byte[] response ) throws SaslException
{
if ( isComplete() )
{
throw new IllegalStateException( "Authentication exchange already completed." );
}
complete = true;
// Verify server-wide policy.
if ( !XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed() )
{
throw new SaslException( "Authentication failed" );
}
// Verify that client can connect from his IP address.
final boolean forbidAccess = !LocalClientSession.isAllowedAnonymous( session.getConnection() );
if ( forbidAccess )
{
throw new SaslException( "Authentication failed" );
}
// Just accept the authentication :)
return null;
}
@Override
public boolean isComplete()
{
return complete;
}
@Override
public String getAuthorizationID()
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
return null; // Anonymous!
}
@Override
public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public Object getNegotiatedProperty( String propName )
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
if ( propName.equals( Sasl.QOP ) )
{
return "auth";
}
else
{
return null;
}
}
@Override
public void dispose() throws SaslException
{
complete = false;
session = null;
}
}
package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
/**
* Implementation of the SASL EXTERNAL mechanism with PKIX to be used for client-to-server connections.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see <a href="http://tools.ietf.org/html/rfc6125">RFC 6125</a>
* @see <a href="http://xmpp.org/extensions/xep-0178.html">XEP 0178</a>
*/
public class ExternalClientSaslServer implements SaslServer
{
public static final Logger Log = LoggerFactory.getLogger( ExternalClientSaslServer.class );
public static final String NAME = "EXTERNAL";
private boolean complete = false;
private String authorizationID = null;
private LocalClientSession session;
public ExternalClientSaslServer( LocalClientSession session ) throws SaslException
{
this.session = session;
}
@Override
public String getMechanismName()
{
return NAME;
}
@Override
public byte[] evaluateResponse( byte[] response ) throws SaslException
{
if ( isComplete() )
{
throw new IllegalStateException( "Authentication exchange already completed." );
}
// There will be no further steps. Either authentication succeeds or fails, but in any case, we're done.
complete = true;
final Connection connection = session.getConnection();
if ( connection.getPeerCertificates().length < 1 )
{
throw new SaslException( "No peer certificates." );
}
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( connection.getPeerCertificates(), keyStore, trustStore );
if ( trusted == null )
{
throw new SaslException( "Certificate chain of peer is not trusted." );
}
// Process client identities / principals.
final ArrayList<String> principals = new ArrayList<>();
principals.addAll( CertificateManager.getClientIdentities( trusted ) );
String principal;
switch ( principals.size() )
{
case 0:
principal = "";
break;
default:
Log.debug( "More than one principal found, using the first one." );
// intended fall-through;
case 1:
principal = principals.get( 0 );
break;
}
// Process requested user name.
String username;
if ( response != null && response.length > 0 )
{
username = new String( response, StandardCharsets.UTF_8 );
}
else
{
username = null;
}
if ( username == null || username.length() == 0 )
{
// No username was provided, according to XEP-0178 we need to:
// * attempt to get it from the cert first
// * have the server assign one
// There shouldn't be more than a few principals in here. One ideally. We set principal to the first one in
// the list to have a sane default. If this list is empty, then the cert had no identity at all, which will
// cause an authorization failure.
for ( String princ : principals )
{
final String mappedUsername = AuthorizationManager.map( princ );
if ( !mappedUsername.equals( princ ) )
{
username = mappedUsername;
principal = princ;
break;
}
}
if ( username == null || username.length() == 0 )
{
// Still no username. Punt.
username = principal;
}
Log.debug( "No username requested, using: {}", username );
}
// Its possible that either/both username and principal are null here. The providers should not allow a null authorization
if ( AuthorizationManager.authorize( username, principal ) )
{
Log.debug( "Principal {} authorized to username {}", principal, username );
authorizationID = username;
return null; // Success!
}
throw new SaslException();
}
@Override
public boolean isComplete()
{
return complete;
}
@Override
public String getAuthorizationID()
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
return authorizationID;
}
@Override
public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public Object getNegotiatedProperty( String propName )
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
if ( propName.equals( Sasl.QOP ) )
{
return "auth";
}
else
{
return null;
}
}
@Override
public void dispose() throws SaslException
{
complete = false;
authorizationID = null;
session = null;
}
}
package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
/**
* Implementation of the SASL EXTERNAL mechanism with PKIX to be used for server-to-server connections.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see <a href="http://tools.ietf.org/html/rfc6125">RFC 6125</a>
* @see <a href="http://xmpp.org/extensions/xep-0178.html">XEP 0178</a>
*/
public class ExternalServerSaslServer implements SaslServer
{
public static final String NAME = "EXTERNAL";
private boolean complete = false;
private String authorizationID = null;
private LocalIncomingServerSession session;
public ExternalServerSaslServer( LocalIncomingServerSession session ) throws SaslException
{
this.session = session;
}
@Override
public String getMechanismName()
{
return NAME;
}
@Override
public byte[] evaluateResponse( byte[] response ) throws SaslException
{
if ( isComplete() )
{
throw new IllegalStateException( "Authentication exchange already completed." );
}
if ( response == null || response.length == 0 )
{
// No hostname was provided so send a challenge to get it
return new byte[ 0 ];
}
complete = true;
final String requestedId = new String( response, StandardCharsets.UTF_8 );
final String defaultIdentity = session.getDefaultIdentity();
if ( !requestedId.equals( defaultIdentity ) )
{
throw new SaslException( "From '" + requestedId + "' does not equal authzid '" + defaultIdentity + "'" );
}
authorizationID = requestedId;
return null; // Success!
}
@Override
public boolean isComplete()
{
return complete;
}
@Override
public String getAuthorizationID()
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
return authorizationID;
}
@Override
public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public Object getNegotiatedProperty( String propName )
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
if ( propName.equals( Sasl.QOP ) )
{
return "auth";
}
else
{
return null;
}
}
@Override
public void dispose() throws SaslException
{
complete = false;
authorizationID = null;
session = null;
}
}
package org.jivesoftware.openfire.sasl;
/**
* XMPP specified SASL errors.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see <a href="http://tools.ietf.org/html/rfc6120#section-6.5">RFC 6120 section 6.5</a>
*/
public enum Failure
{
ABORTED( "aborted" ),
ACCOUNT_DISABLED( "account-disabled" ),
CREDENTIALS_EXPIRED( "credentials-expired" ),
ENCRYPTION_REQUIRED( "encryption-required" ),
INCORRECT_ENCODING( "incorrect-encoding" ),
INVALID_AUTHZID( "invalid-authzid" ),
INVALID_MECHANISM( "invalid-mechanism" ),
MALFORMED_REQUEST( "malformed-request" ),
MECHANISM_TOO_WEAK( "mechanism-too-weak" ),
NOT_AUTHORIZED( "not-authorized" ),
TEMPORARY_AUTH_FAILURE( "temporary-auth-failure" );
private String name = null;
Failure( String name )
{
this.name = name;
}
@Override
public String toString()
{
return name;
}
}
package org.jivesoftware.openfire.sasl;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;
/**
* Implementation of a proprietary Jive Software SASL mechanism that is based on a shared secret. Successful
* authentication will result in an anonymous authorization.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class JiveSharedSecretSaslServer implements SaslServer
{
public static final String NAME = "JIVE-SHAREDSECRET";
private boolean complete = false;
@Override
public String getMechanismName()
{
return NAME;
}
@Override
public byte[] evaluateResponse( byte[] response ) throws SaslException
{
if ( isComplete() )
{
throw new IllegalStateException( "Authentication exchange already completed." );
}
if ( response == null || response.length == 0 )
{
// No info was provided so send a challenge to get it.
return new byte[ 0 ];
}
complete = true;
// Parse data and obtain username & password.
final StringTokenizer tokens = new StringTokenizer( new String( response, StandardCharsets.UTF_8 ), "\0" );
tokens.nextToken();
final String secretDigest = tokens.nextToken();
if ( authenticateSharedSecret( secretDigest ) )
{
return null; // Success!
}
else
{
// Otherwise, authentication failed.
throw new SaslException( "Authentication failed" );
}
}
@Override
public boolean isComplete()
{
return complete;
}
@Override
public String getAuthorizationID()
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
return null; // Anonymous!
}
@Override
public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." );
}
@Override
public Object getNegotiatedProperty( String propName )
{
if ( !isComplete() )
{
throw new IllegalStateException( "Authentication exchange not completed." );
}
if ( propName.equals( Sasl.QOP ) )
{
return "auth";
}
else
{
return null;
}
}
@Override
public void dispose() throws SaslException
{
complete = false;
}
/**
* Returns true if the supplied digest matches the shared secret value. The digest must be an MD5 hash of the secret
* key, encoded as hex. This value is supplied by clients attempting shared secret authentication.
*
* @param digest the MD5 hash of the secret key, encoded as hex.
* @return true if authentication succeeds.
*/
public static boolean authenticateSharedSecret( String digest )
{
if ( !isSharedSecretAllowed() )
{
return false;
}
return StringUtils.hash( getSharedSecret() ).equals( digest );
}
/**
* Returns true if shared secret authentication is enabled. Shared secret authentication creates an anonymous
* session, but requires that the authenticating entity know a shared secret key. The client sends a digest of the
* secret key, which is compared against a digest of the local shared key.
*
* @return true if shared secret authentication is enabled.
*/
public static boolean isSharedSecretAllowed()
{
return JiveGlobals.getBooleanProperty( "xmpp.auth.sharedSecretEnabled" );
}
/**
* Returns the shared secret value, or <tt>null</tt> if shared secret authentication is disabled. If this is the
* first time the shared secret value has been requested (and shared secret auth is enabled), the key will be
* randomly generated and stored in the property <tt>xmpp.auth.sharedSecret</tt>.
*
* @return the shared secret value.
*/
public static String getSharedSecret()
{
if ( !isSharedSecretAllowed() )
{
return null;
}
String sharedSecret = JiveGlobals.getProperty( "xmpp.auth.sharedSecret" );
if ( sharedSecret == null )
{
sharedSecret = StringUtils.randomString( 8 );
JiveGlobals.setProperty( "xmpp.auth.sharedSecret", sharedSecret );
}
return sharedSecret;
}
/**
* Sets whether shared secret authentication is enabled. Shared secret authentication creates an anonymous session,
* but requires that the authenticating entity know a shared secret key. The client sends a digest of the secret
* key, which is compared against a digest of the local shared key.
*
* @param sharedSecretAllowed true if shared secret authentication should be enabled.
*/
public static void setSharedSecretAllowed( boolean sharedSecretAllowed )
{
JiveGlobals.setProperty( "xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false" );
}
}
package org.jivesoftware.openfire.sasl;
import javax.security.sasl.SaslException;
/**
* A SaslException with XMPP 'failure' context.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class SaslFailureException extends SaslException
{
private final Failure failure;
public SaslFailureException( Failure failure, String message )
{
super( message );
this.failure = failure;
}
public SaslFailureException( Failure failure )
{
this.failure = failure;
}
public SaslFailureException( String detail, Failure failure )
{
super( detail );
this.failure = failure;
}
public SaslFailureException( String detail, Throwable ex, Failure failure )
{
super( detail, ex );
this.failure = failure;
}
public Failure getFailure()
{
return failure;
}
}
......@@ -20,7 +20,16 @@
package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.security.auth.callback.CallbackHandler;
import javax.security.sasl.Sasl;
......@@ -34,72 +43,125 @@ import javax.security.sasl.SaslServerFactory;
* @author Jay Kline
*/
public class SaslServerFactoryImpl implements SaslServerFactory {
private static final String myMechs[] = { "PLAIN", "SCRAM-SHA-1" };
private static final int PLAIN = 0;
private static final int SCRAM_SHA_1 = 1;
public SaslServerFactoryImpl() {
}
public class SaslServerFactoryImpl implements SaslServerFactory
{
private final static Logger Log = LoggerFactory.getLogger( SaslServerFactoryImpl.class );
/**
* Creates a <code>SaslServer</code> implementing a supported mechanism using the parameters supplied.
*
* @param mechanism The non-null IANA-registered named of a SASL mechanism.
* @param protocol The non-null string name of the protocol for which the authentication is being performed (e.g., "ldap").
* @param serverName The non-null fully qualified host name of the server to authenticate to.
* @param props The possibly null set of properties used to select the SASL mechanism and to configure the authentication exchange of the selected mechanism.
* @param cbh The possibly null callback handler to used by the SASL mechanisms to get further information from the application/library to complete the authentication.
* @return A possibly null SaslServer created using the parameters supplied. If null, this factory cannot produce a SaslServer using the parameters supplied.
* @throws SaslException If cannot create a SaslServer because of an error.
* All mechanisms provided by this factory.
*/
private final Set<Mechanism> allMechanisms;
@Override
public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh) throws SaslException {
if (mechanism.equals(myMechs[PLAIN]) && checkPolicy(props)) {
if (cbh == null) {
throw new SaslException("CallbackHandler with support for Password, Name, and AuthorizeCallback required");
public SaslServerFactoryImpl()
{
allMechanisms = new HashSet<>();
allMechanisms.add( new Mechanism( "PLAIN", true, true ) );
allMechanisms.add( new Mechanism( "SCRAM_SHA_1", false, false ) );
allMechanisms.add( new Mechanism( "JIVE-SHAREDSECRET", true, false ) );
allMechanisms.add( new Mechanism( "EXTERNAL", false, false ) );
}
return new SaslServerPlainImpl(protocol, serverName, props, cbh);
@Override
public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh) throws SaslException
{
if ( !Arrays.asList( getMechanismNames( props )).contains( mechanism ) )
{
Log.debug( "This implementation is unable to create a SaslServer instance for the {} mechanism using the provided properties.", mechanism );
return null;
}
else if (mechanism.equals(myMechs[SCRAM_SHA_1])) {
if (cbh == null) {
throw new SaslException("CallbackHandler with support for AuthorizeCallback required");
switch ( mechanism.toUpperCase() )
{
case "PLAIN":
if ( cbh != null )
{
Log.debug( "Unable to instantiate {} SaslServer: A callbackHandler with support for Password, Name, and AuthorizeCallback required.", mechanism );
return null;
}
return new SaslServerPlainImpl( protocol, serverName, props, cbh );
case "SCRAM_SHA_1":
return new ScramSha1SaslServer();
}
case "ANONYMOUS":
if ( !props.containsKey( LocalSession.class.getCanonicalName() ) )
{
Log.debug( "Unable to instantiate {} SaslServer: Provided properties do not contain a LocalSession instance.", mechanism );
return null;
}
else
{
final LocalSession session = (LocalSession) props.get( LocalSession.class.getCanonicalName() );
return new AnonymousSaslServer( session );
}
/**
* Requires supported mechanisms to allow anonymous logins
*
* @param props The security properties to check
* @return true if the policy allows anonymous logins
*/
private boolean checkPolicy(Map<String, ?> props) {
boolean result = true;
if (props != null) {
String policy = (String) props.get(Sasl.POLICY_NOANONYMOUS);
if (Boolean.parseBoolean(policy)) {
result = false;
case "EXTERNAL":
if ( !props.containsKey( LocalSession.class.getCanonicalName() ) )
{
Log.debug( "Unable to instantiate {} SaslServer: Provided properties do not contain a LocalSession instance.", mechanism );
return null;
}
else
{
final Object session = props.get( LocalSession.class.getCanonicalName() );
if ( session instanceof LocalClientSession )
{
return new ExternalClientSaslServer( (LocalClientSession) session );
}
return result;
if ( session instanceof LocalIncomingServerSession )
{
return new ExternalServerSaslServer( (LocalIncomingServerSession) session );
}
/**
* Returns an array of names of mechanisms that match the specified mechanism selection policies.
* @param props The possibly null set of properties used to specify the security policy of the SASL mechanisms.
* @return A non-null array containing a IANA-registered SASL mechanism names.
*/
Log.debug( "Unable to instantiate {} Sasl Server: Provided properties contains neither LocalClientSession nor LocalIncomingServerSession instance.", mechanism );
return null;
}
case JiveSharedSecretSaslServer.NAME:
return new JiveSharedSecretSaslServer();
default:
throw new IllegalStateException(); // Fail fast - this should not be possible, as the first check in this method already verifies wether the mechanism is supported.
}
}
@Override
public String[] getMechanismNames(Map<String, ?> props) {
if (checkPolicy(props)) {
return myMechs;
public String[] getMechanismNames( Map<String, ?> props )
{
final Set<String> result = new HashSet<>();
for ( final Mechanism mechanism : allMechanisms )
{
if ( mechanism.allowsAnonymous && props.containsKey( Sasl.POLICY_NOANONYMOUS ) && Boolean.parseBoolean( (String) props.get( Sasl.POLICY_NOANONYMOUS ) ) )
{
// Do not include a mechanism that allows anonymous authentication when the 'no anonymous' policy is set.
continue;
}
if ( mechanism.isPlaintext && props.containsKey( Sasl.POLICY_NOPLAINTEXT ) && Boolean.parseBoolean( (String) props.get( Sasl.POLICY_NOPLAINTEXT ) ) )
{
// Do not include a mechanism that is susceptible to simple plain passive attacks when the 'no plaintext' policy is set.
continue;
}
// Mechanism passed all filters. It should be part of the result.
result.add( mechanism.name );
}
return result.toArray( new String[ result.size() ] );
}
private static class Mechanism
{
final String name;
final boolean allowsAnonymous;
final boolean isPlaintext;
private Mechanism( String name, boolean allowsAnonymous, boolean isPlaintext )
{
this.name = name;
this.allowsAnonymous = allowsAnonymous;
this.isPlaintext = isPlaintext;
}
return new String [] { };
}
}
......@@ -55,6 +55,7 @@ import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.openfire.fastpath.util.WorkgroupUtils;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
......@@ -213,8 +214,8 @@ public class WorkgroupManager implements Component {
// We use a custom SASL mechanism so that web-based customer chats can login without
// a username or password. However, a shared secret key is still required so that
// anonymous login doesn't have to be enabled for the whole server.
if (!SASLAuthentication.isSharedSecretAllowed()) {
SASLAuthentication.setSharedSecretAllowed(true);
if (!JiveSharedSecretSaslServer.isSharedSecretAllowed()) {
JiveSharedSecretSaslServer.setSharedSecretAllowed( true );
}
// If the database was just created then create the "demo" user and "demo" workgroup
......
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