Commit 2cd0137f authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1097: Reduce complexity of AuthProvider

AuthProvider has quite some complexity that exists solely for XEP-0078.
This commit removes most of that, by replacing the generic checks for
digest and plain support with a non-generic implementation, that will
work for any auth provider that supports password retrieval.
parent 667cd414
......@@ -24,6 +24,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.Blowfish;
......@@ -146,27 +147,6 @@ public class AuthFactory {
return authProvider.supportsPasswordRetrieval();
}
/**
* Returns true if the currently installed {@link AuthProvider} supports authentication
* using plain-text passwords according to JEP-0078. Plain-text authentication is
* not secure and should generally only be used over a TLS/SSL connection.
*
* @return true if plain text password authentication is supported.
*/
public static boolean isPlainSupported() {
return authProvider.isPlainSupported();
}
/**
* Returns true if the currently installed {@link AuthProvider} supports
* digest authentication according to JEP-0078.
*
* @return true if digest authentication is supported.
*/
public static boolean isDigestSupported() {
return authProvider.isDigestSupported();
}
/**
* Returns the user's password. This method will throw an UnsupportedOperationException
* if this operation is not supported by the backend user store.
......@@ -218,30 +198,6 @@ public class AuthFactory {
return new AuthToken(username);
}
/**
* Authenticates a user with a username, token, and digest and returns an AuthToken.
* The digest should be generated using the {@link #createDigest(String, String)} method.
* If the username and digest do not match the record of any user in the system, the
* method throws an UnauthorizedException.
*
* @param username the username.
* @param token the token that was used with plain-text password to generate the digest.
* @param digest the digest generated from plain-text password and unique token.
* @return an AuthToken token if the username and digest are correct for the user's
* password and given token.
* @throws UnauthorizedException if the username and password do not match any
* existing user or the account is locked out.
*/
public static AuthToken authenticate(String username, String token, String digest)
throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException {
if (LockOutManager.getInstance().isAccountDisabled(username)) {
LockOutManager.getInstance().recordFailedLogin(username);
throw new UnauthorizedException();
}
authProvider.authenticate(username, token, digest);
return new AuthToken(username);
}
/**
* Returns a digest given a token and password, according to JEP-0078.
*
......
......@@ -39,32 +39,10 @@ import org.jivesoftware.openfire.user.UserNotFoundException;
*/
public interface AuthProvider {
/**
* Returns true if this AuthProvider supports authentication using plain-text
* passwords according to JEP--0078. Plain text authentication is not secure
* and should generally only be used for a TLS/SSL connection.
*
* @return true if plain text password authentication is supported by
* this AuthProvider.
*/
boolean isPlainSupported();
/**
* Returns true if this AuthProvider supports digest authentication
* according to JEP-0078.
*
* @return true if digest authentication is supported by this
* AuthProvider.
*/
boolean isDigestSupported();
/**
* Returns if the username and password are valid; otherwise this
* method throws an UnauthorizedException.<p>
*
* If {@link #isPlainSupported()} returns false, this method should
* throw an UnsupportedOperationException.
*
* @param username the username or full JID.
* @param password the password
* @throws UnauthorizedException if the username and password do
......@@ -75,25 +53,6 @@ public interface AuthProvider {
void authenticate(String username, String password) throws UnauthorizedException,
ConnectionException, InternalUnauthenticatedException;
/**
* Returns if the username, token, and digest are valid; otherwise this
* method throws an UnauthorizedException.<p>
*
* If {@link #isDigestSupported()} returns false, this method should
* throw an UnsupportedOperationException.
*
* @param username the username or full JID.
* @param token the token that was used with plain-text password to
* generate the digest.
* @param digest the digest generated from plain-text password and unique token.
* @throws UnauthorizedException if the username and password
* do not match any existing user.
* @throws ConnectionException it there is a problem connecting to user and group sytem
* @throws InternalUnauthenticatedException if there is a problem authentication Openfire iteself into the user and group system
*/
void authenticate(String username, String token, String digest)
throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException;
/**
* Returns the user's password. This method should throw an UnsupportedOperationException
* if this operation is not supported by the backend user store.
......
......@@ -96,47 +96,6 @@ public class DefaultAuthProvider implements AuthProvider {
// Got this far, so the user must be authorized.
}
@Override
public void authenticate(String username, String token, String digest) throws UnauthorizedException {
if (username == null || token == null || digest == null) {
throw new UnauthorizedException();
}
username = username.trim().toLowerCase();
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
int index = username.indexOf("@");
String domain = username.substring(index + 1);
if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
username = username.substring(0, index);
} else {
// Unknown domain. Return authentication failed.
throw new UnauthorizedException();
}
}
try {
String password = getPassword(username);
String anticipatedDigest = AuthFactory.createDigest(token, password);
if (!digest.equalsIgnoreCase(anticipatedDigest)) {
throw new UnauthorizedException();
}
}
catch (UserNotFoundException unfe) {
throw new UnauthorizedException();
}
// Got this far, so the user must be authorized.
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
boolean scramOnly = JiveGlobals.getBooleanProperty("user.scramHashedPasswordOnly");
return !scramOnly;
}
@Override
public String getPassword(String username) throws UserNotFoundException {
if (!supportsPasswordRetrieval()) {
......
......@@ -106,13 +106,6 @@ public class HybridAuthProvider implements AuthProvider {
try {
Class c = ClassUtils.forName(primaryClass);
primaryProvider = (AuthProvider)c.newInstance();
// All providers must support plain auth.
if (!primaryProvider.isPlainSupported()) {
Log.error("Provider " + primaryClass + " must support plain authentication. " +
"Authentication disabled.");
primaryProvider = null;
return;
}
Log.debug("Primary auth provider: " + primaryClass);
}
catch (Exception e) {
......@@ -127,14 +120,6 @@ public class HybridAuthProvider implements AuthProvider {
try {
Class c = ClassUtils.forName(secondaryClass);
secondaryProvider = (AuthProvider)c.newInstance();
// All providers must support plain auth.
if (!secondaryProvider.isPlainSupported()) {
Log.error("Provider " + secondaryClass + " must support plain authentication. " +
"Authentication disabled.");
primaryProvider = null;
secondaryProvider = null;
return;
}
Log.debug("Secondary auth provider: " + secondaryClass);
}
catch (Exception e) {
......@@ -148,15 +133,6 @@ public class HybridAuthProvider implements AuthProvider {
try {
Class c = ClassUtils.forName(tertiaryClass);
tertiaryProvider = (AuthProvider)c.newInstance();
// All providers must support plain auth.
if (!tertiaryProvider.isPlainSupported()) {
Log.error("Provider " + tertiaryClass + " must support plain authentication. " +
"Authentication disabled.");
primaryProvider = null;
secondaryProvider = null;
tertiaryProvider = null;
return;
}
Log.debug("Tertiary auth provider: " + tertiaryClass);
}
catch (Exception e) {
......@@ -188,16 +164,6 @@ public class HybridAuthProvider implements AuthProvider {
}
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
return false;
}
@Override
public void authenticate(String username, String password) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException {
// Check overrides first.
......@@ -238,13 +204,6 @@ public class HybridAuthProvider implements AuthProvider {
}
}
@Override
public void authenticate(String username, String token, String digest)
throws UnauthorizedException
{
throw new UnauthorizedException("Digest authentication not supported.");
}
@Override
public String getPassword(String username)
throws UserNotFoundException, UnsupportedOperationException
......
......@@ -258,57 +258,6 @@ public class JDBCAuthProvider implements AuthProvider, PropertyEventListener {
}
}
@Override
public void authenticate(String username, String token, String digest)
throws UnauthorizedException
{
if (passwordTypes.size() != 1 || passwordTypes.get(0) != PasswordType.plain) {
throw new UnsupportedOperationException("Digest authentication not supported for "
+ "password type " + passwordTypes.get(0));
}
if (username == null || token == null || digest == null) {
throw new UnauthorizedException();
}
username = username.trim().toLowerCase();
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
int index = username.indexOf("@");
String domain = username.substring(index + 1);
if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
username = username.substring(0, index);
} else {
// Unknown domain. Return authentication failed.
throw new UnauthorizedException();
}
}
String password;
try {
password = getPasswordValue(username);
}
catch (UserNotFoundException unfe) {
throw new UnauthorizedException();
}
String anticipatedDigest = AuthFactory.createDigest(token, password);
if (!digest.equalsIgnoreCase(anticipatedDigest)) {
throw new UnauthorizedException();
}
// Got this far, so the user must be authorized.
createUser(username);
}
@Override
public boolean isPlainSupported() {
// If the auth SQL is defined, plain text authentication is supported.
return (passwordSQL != null);
}
@Override
public boolean isDigestSupported() {
// The auth SQL must be defined and the password type is supported.
return (passwordSQL != null && passwordTypes.size() == 1 && passwordTypes.get(0) == PasswordType.plain);
}
@Override
public String getPassword(String username) throws UserNotFoundException,
UnsupportedOperationException
......
......@@ -182,23 +182,6 @@ public class NativeAuthProvider implements AuthProvider {
}
}
@Override
public void authenticate(String username, String token, String digest)
throws UnauthorizedException
{
throw new UnsupportedOperationException();
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
return false;
}
@Override
public String getPassword(String username)
throws UserNotFoundException, UnsupportedOperationException
......
......@@ -219,23 +219,6 @@ public class POP3AuthProvider implements AuthProvider {
}
}
@Override
public void authenticate(String username, String token, String digest)
throws UnauthorizedException
{
throw new UnauthorizedException("Digest authentication not supported.");
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
return false;
}
@Override
public String getPassword(String username)
throws UserNotFoundException, UnsupportedOperationException
......
......@@ -43,16 +43,6 @@ public class CrowdAuthProvider implements AuthProvider {
}
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
return false;
}
/**
* Returns if the username and password are valid; otherwise this
* method throws an UnauthorizedException.<p>
......@@ -96,11 +86,6 @@ public class CrowdAuthProvider implements AuthProvider {
}
}
@Override
public void authenticate(String username, String token, String digest) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException {
throw new UnsupportedOperationException("XMPP digest authentication not supported by this version of authentication provider");
}
@Override
public String getPassword(String username) throws UserNotFoundException, UnsupportedOperationException {
throw new UnsupportedOperationException("Retrieve password not supported by this version of authentication provider");
......
......@@ -71,16 +71,6 @@ public class LdapAuthProvider implements AuthProvider {
}
}
@Override
public boolean isPlainSupported() {
return true;
}
@Override
public boolean isDigestSupported() {
return false;
}
@Override
public void authenticate(String username, String password) throws UnauthorizedException {
if (username == null || password == null || "".equals(password.trim())) {
......@@ -146,11 +136,6 @@ public class LdapAuthProvider implements AuthProvider {
}
}
@Override
public void authenticate(String username, String token, String digest) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Digest authentication not currently supported.");
}
@Override
public String getPassword(String username) throws UserNotFoundException,
UnsupportedOperationException
......
......@@ -127,7 +127,7 @@ public class SASLAuthentication {
/**
* SASL negotiation has been successful.
*/
authenticated;
authenticated
}
/**
......@@ -230,7 +230,7 @@ public class SASLAuthentication {
// Construct the configuration properties
final Map<String, Object> props = new HashMap<>();
props.put( LocalClientSession.class.getCanonicalName(), session );
props.put( Sasl.POLICY_NOANONYMOUS, Boolean.toString( !XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed() ) );
props.put( Sasl.POLICY_NOANONYMOUS, Boolean.toString( !JiveGlobals.getBooleanProperty( "xmpp.auth.anonymous" ) ) );
SaslServer saslServer = Sasl.createSaslServer( mechanismName, "xmpp", session.getServerName(), props, new XMPPCallbackHandler() );
if ( saslServer == null )
......@@ -437,7 +437,7 @@ 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 name of the new SASL mechanism (cannot be null or an empty String).
* @param mechanismName the name of the new SASL mechanism (cannot be null or an empty String).
*/
public static void addSupportedMechanism(String mechanismName) {
if ( mechanismName == null || mechanismName.isEmpty() ) {
......@@ -490,7 +490,7 @@ public class SASLAuthentication {
}
else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
if (!JiveGlobals.getBooleanProperty( "xmpp.auth.anonymous" )) {
it.remove();
}
}
......
......@@ -3,6 +3,7 @@ package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.util.JiveGlobals;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
......@@ -45,7 +46,7 @@ public class AnonymousSaslServer implements SaslServer
complete = true;
// Verify server-wide policy.
if ( !XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed() )
if ( !JiveGlobals.getBooleanProperty( "xmpp.auth.anonymous" ) )
{
throw new SaslException( "Authentication failed" );
}
......
......@@ -40,6 +40,7 @@ import org.jivesoftware.openfire.auth.ConnectionException;
import org.jivesoftware.openfire.auth.InternalUnauthenticatedException;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.event.SessionEventDispatcher;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.Session;
......@@ -94,10 +95,8 @@ public class IQAuthHandler extends IQHandler {
probeResponse = DocumentHelper.createElement(QName.get("query", "jabber:iq:auth"));
probeResponse.addElement("username");
if (AuthFactory.isPlainSupported()) {
if (AuthFactory.supportsPasswordRetrieval()) {
probeResponse.addElement("password");
}
if (AuthFactory.isDigestSupported()) {
probeResponse.addElement("digest");
}
probeResponse.addElement("resource");
......@@ -250,12 +249,12 @@ public class IQAuthHandler extends IQHandler {
username = username.toLowerCase();
// Verify that supplied username and password are correct (i.e. user authentication was successful)
AuthToken token = null;
if (password != null && AuthFactory.isPlainSupported()) {
token = AuthFactory.authenticate(username, password);
if ( AuthFactory.supportsPasswordRetrieval() ) {
if ( password != null) {
token = AuthFactory.authenticate( username, password );
} else if ( digest != null) {
token = authenticate(username, session.getStreamID().toString(), digest );
}
else if (digest != null && AuthFactory.isDigestSupported()) {
token = AuthFactory.authenticate(username, session.getStreamID().toString(),
digest);
}
if (token == null) {
throw new UnauthorizedException();
......@@ -361,4 +360,54 @@ public class IQAuthHandler extends IQHandler {
public IQHandlerInfo getInfo() {
return info;
}
/**
* Authenticates a user with a username, token, and digest and returns an AuthToken.
* The digest should be generated using the {@link AuthFactory#createDigest(String, String)} method.
* If the username and digest do not match the record of any user in the system, the
* method throws an UnauthorizedException.
*
* @param username the username.
* @param token the token that was used with plain-text password to generate the digest.
* @param digest the digest generated from plain-text password and unique token.
* @return an AuthToken token if the username and digest are correct for the user's
* password and given token.
* @throws UnauthorizedException if the username and password do not match any
* existing user or the account is locked out.
*/
public static AuthToken authenticate(String username, String token, String digest)
throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException {
if (username == null || token == null || digest == null) {
throw new UnauthorizedException();
}
if ( LockOutManager.getInstance().isAccountDisabled(username)) {
LockOutManager.getInstance().recordFailedLogin(username);
throw new UnauthorizedException();
}
username = username.trim().toLowerCase();
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
int index = username.indexOf("@");
String domain = username.substring(index + 1);
if (domain.equals( XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
username = username.substring(0, index);
} else {
// Unknown domain. Return authentication failed.
throw new UnauthorizedException();
}
}
try {
String password = AuthFactory.getPassword( username );
String anticipatedDigest = AuthFactory.createDigest(token, password);
if (!digest.equalsIgnoreCase(anticipatedDigest)) {
throw new UnauthorizedException();
}
}
catch (UserNotFoundException unfe) {
throw new UnauthorizedException();
}
// Got this far, so the user must be authorized.
return new AuthToken(username);
}
}
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