1
0
mirror of synced 2026-05-22 21:33:16 +00:00

SEC-1181: Added option to parse AD sub-error codes.

This commit is contained in:
Luke Taylor
2011-04-11 14:01:15 +01:00
parent 428991d997
commit 59ac4c8b96
3 changed files with 175 additions and 27 deletions
@@ -30,6 +30,10 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
JdbcDaoImpl.notFound=User {0} not found JdbcDaoImpl.notFound=User {0} not found
LdapAuthenticationProvider.badCredentials=Bad credentials LdapAuthenticationProvider.badCredentials=Bad credentials
LdapAuthenticationProvider.credentialsExpired=User credentials have expired
LdapAuthenticationProvider.disabled=User is disabled
LdapAuthenticationProvider.expired=User account has expired
LdapAuthenticationProvider.locked=User account is locked
LdapAuthenticationProvider.emptyUsername=Empty username not allowed LdapAuthenticationProvider.emptyUsername=Empty username not allowed
LdapAuthenticationProvider.onlySupports=Only UsernamePasswordAuthenticationToken is supported LdapAuthenticationProvider.onlySupports=Only UsernamePasswordAuthenticationToken is supported
PasswordComparisonAuthenticator.badCredentials=Bad credentials PasswordComparisonAuthenticator.badCredentials=Bad credentials
@@ -39,4 +43,4 @@ RememberMeAuthenticationProvider.incorrectKey=The presented RememberMeAuthentica
RunAsImplAuthenticationProvider.incorrectKey=The presented RunAsUserToken does not contain the expected key RunAsImplAuthenticationProvider.incorrectKey=The presented RunAsUserToken does not contain the expected key
SubjectDnX509PrincipalExtractor.noMatching=No matching pattern was found in subjectDN: {0} SubjectDnX509PrincipalExtractor.noMatching=No matching pattern was found in subjectDN: {0}
SwitchUserFilter.noCurrentUser=No current user associated with this request SwitchUserFilter.noCurrentUser=No current user associated with this request
SwitchUserFilter.noOriginalAuthentication=Could not find original Authentication object SwitchUserFilter.noOriginalAuthentication=Could not find original Authentication object
@@ -2,42 +2,84 @@ package org.springframework.security.ldap.authentication.ad;
import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.LdapUtils;
import org.springframework.security.ldap.SpringSecurityLdapTemplate; import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.naming.AuthenticationException;
import javax.naming.Context; import javax.naming.Context;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapContext;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Specialized LDAP authentication provider which uses Active Directory configuration conventions. * Specialized LDAP authentication provider which uses Active Directory configuration conventions.
* <p> * <p>
* It will authenticate using the Active Directory {@code userPrincipalName} (in the form {@code username@domain}). * It will authenticate using the Active Directory
* If the {@code usernameIncludesDomain} property is set to {@code true}, it is assumed that the user types in the * <a href="http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx">{@code userPrincipalName}</a>
* full value, including the domain. Otherwise (the default), the {@code userPrincipalName} will be built from the * (in the form {@code username@domain}). If the username does not already end with the domain name, the
* username supplied in the authentication request. * {@code userPrincipalName} will be built by appending the configured domain name to the username supplied in the
* authentication request. If no domain name is configured, it is assumed that the username will always contain the
* domain name.
* <p> * <p>
* The user authorities are obtained from the data contained in the {@code memberOf} attribute. * The user authorities are obtained from the data contained in the {@code memberOf} attribute.
* *
* <h3>Active Directory Sub-Error Codes</h3>
*
* When an authentication fails, resulting in a standard LDAP 49 error code, Active Directory also supplies its own
* sub-error codes within the error message. These will be used to provide additional log information on why an
* authentication has failed. Typical examples are
*
* <ul>
* <li>525 - user not found</li>
* <li>52e - invalid credentials</li>
* <li>530 - not permitted to logon at this time</li>
* <li>532 - password expired</li>
* <li>533 - account disabled</li>
* <li>701 - account expired</li>
* <li>773 - user must reset password</li>
* <li>775 - account locked</li>
* </ul>
*
* If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) convertSubErrorCodesToExceptions} property to
* {@code true}, the codes will also be used to control the exception raised.
*
* @author Luke Taylor * @author Luke Taylor
* @since 3.1 * @since 3.1
*/ */
public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider {
private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*\\s([0-9a-f]{3,4}).*");
// Error codes
private static final int USERNAME_NOT_FOUND = 0x525;
private static final int INVALID_PASSWORD = 0x52e;
private static final int NOT_PERMITTED = 0x530;
private static final int PASSWORD_EXPIRED = 0x532;
private static final int ACCOUNT_DISABLED = 0x533;
private static final int ACCOUNT_EXPIRED = 0x701;
private static final int PASSWORD_NEEDS_RESET = 0x773;
private static final int ACCOUNT_LOCKED = 0x775;
private final String domain; private final String domain;
private final String rootDn; private final String rootDn;
private final String url; private final String url;
private boolean usernameIncludesDomain = false; private boolean convertSubErrorCodesToExceptions;
/** /**
* @param domain the domain for which authentication should take place * @param domain the domain for which authentication should take place
@@ -52,7 +94,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/ */
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(domain) || StringUtils.hasText(url), "Domain and url cannot both be empty"); Assert.isTrue(StringUtils.hasText(domain) || StringUtils.hasText(url), "Domain and url cannot both be empty");
this.domain = StringUtils.hasText(domain) ? domain : null; this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
this.url = StringUtils.hasText(url) ? url : null; this.url = StringUtils.hasText(url) ? url : null;
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain); rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
} }
@@ -68,8 +110,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return searchForUser(ctx, username); return searchForUser(ctx, username);
} catch (NamingException e) { } catch (NamingException e) {
logger.error("Failed to locate directory entry for authentication user: " + username, e); logger.error("Failed to locate directory entry for authenticated user: " + username, e);
throw authenticationFailure(); throw badCredentials();
} finally { } finally {
LdapUtils.closeContext(ctx); LdapUtils.closeContext(ctx);
} }
@@ -108,7 +150,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
Hashtable<String,String> env = new Hashtable<String,String>(); Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, createBindPrincipal(username)); String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
env.put(Context.PROVIDER_URL, bindUrl); env.put(Context.PROVIDER_URL, bindUrl);
env.put(Context.SECURITY_CREDENTIALS, password); env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
@@ -116,12 +159,84 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
try { try {
return new InitialLdapContext(env, null); return new InitialLdapContext(env, null);
} catch (NamingException e) { } catch (NamingException e) {
logger.debug("Authentication failed", e); if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) {
throw authenticationFailure(); handleBindException(bindPrincipal, e);
throw badCredentials();
} else {
throw LdapUtils.convertLdapException(e);
}
} }
} }
private BadCredentialsException authenticationFailure() { void handleBindException(String bindPrincipal, NamingException exception) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
}
int subErrorCode = parseSubErrorCode(exception.getMessage());
if (subErrorCode > 0) {
logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));
if (convertSubErrorCodesToExceptions) {
raiseExceptionForErrorCode(subErrorCode);
}
} else {
logger.debug("Failed to locate AD-specific sub-error code in message");
}
}
int parseSubErrorCode(String message) {
Matcher m = SUB_ERROR_CODE.matcher(message);
if (m.matches()) {
return Integer.parseInt(m.group(1), 16);
}
return -1;
}
void raiseExceptionForErrorCode(int code) {
switch (code) {
case PASSWORD_EXPIRED:
throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
case ACCOUNT_DISABLED:
throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled",
"User is disabled"));
case ACCOUNT_EXPIRED:
throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired",
"User account has expired"));
case ACCOUNT_LOCKED:
throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked",
"User account is locked"));
}
}
String subCodeToLogMessage(int code) {
switch (code) {
case USERNAME_NOT_FOUND:
return "User was not found in directory";
case INVALID_PASSWORD:
return "Supplied password was invalid";
case NOT_PERMITTED:
return "User not permitted to logon at this time";
case PASSWORD_EXPIRED:
return "Password has expired";
case ACCOUNT_DISABLED:
return "Account is disabled";
case ACCOUNT_EXPIRED:
return "Account expired";
case PASSWORD_NEEDS_RESET:
return "User must reset password";
case ACCOUNT_LOCKED:
return "Account locked";
}
return "Unknown (error code " + Integer.toHexString(code) +")";
}
private BadCredentialsException badCredentials() {
return new BadCredentialsException(messages.getMessage( return new BadCredentialsException(messages.getMessage(
"LdapAuthenticationProvider.badCredentials", "Bad credentials")); "LdapAuthenticationProvider.badCredentials", "Bad credentials"));
} }
@@ -158,18 +273,27 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return root.toString(); return root.toString();
} }
private String createBindPrincipal(String username) { String createBindPrincipal(String username) {
if (usernameIncludesDomain || domain == null) { if (domain == null || username.toLowerCase().endsWith(domain)) {
return username; return username;
} }
return username + "@" + domain; return username + "@" + domain;
} }
public void setUsernameIncludesDomain(boolean usernameIncludesDomain) { /**
Assert.isTrue(domain != null || usernameIncludesDomain, * By default, a failed authentication (LDAP error 49) will result in a {@code BadCredentialsException}.
"If the domain name is not included in the username, a domain must be set in the constructor"); * <p>
this.usernameIncludesDomain = usernameIncludesDomain; * If this property is set to {@code true}, the exception message from a failed bind attempt will be parsed
* for the AD-specific error code and a {@link CredentialsExpiredException}, {@link DisabledException},
* {@link AccountExpiredException} or {@link LockedException} will be thrown for the corresponding codes. All
* other codes will result in the default {@code BadCredentialsException}.
*
* @param convertSubErrorCodesToExceptions {@code true} to raise an exception based on the AD error code.
*/
public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToExceptions) {
this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions;
} }
} }
@@ -1,25 +1,45 @@
package org.springframework.security.ldap.authentication.ad; package org.springframework.security.ldap.authentication.ad;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import org.junit.*; import org.junit.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.ldap.LdapContext;
import javax.naming.spi.InitialContextFactory;
import javax.naming.spi.InitialContextFactoryBuilder;
import javax.naming.spi.NamingManager;
import java.util.*;
/** /**
* @author Luke Taylor * @author Luke Taylor
*/ */
public class ActiveDirectoryLdapAuthenticationProviderTests { public class ActiveDirectoryLdapAuthenticationProviderTests {
@Test @Test
public void simpleAuthenticationWithIsSucessful() throws Exception { public void bindPrincipalIsCreatedCorrectly() throws Exception {
ActiveDirectoryLdapAuthenticationProvider provider = ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/"); new ActiveDirectoryLdapAuthenticationProvider("mydomain.eu", "ldap://192.168.1.200/");
assertEquals("joe@mydomain.eu", provider.createBindPrincipal("joe"));
Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("luke@fenetres.monkeymachine.eu","p!ssw0rd")); assertEquals("joe@mydomain.eu", provider.createBindPrincipal("joe@mydomain.eu"));
assertEquals(1, result.getAuthorities().size());
assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
} }
// @Test
// public void realAuthenticationIsSucessful() throws Exception {
// ActiveDirectoryLdapAuthenticationProvider provider =
// new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
//
// provider.setConvertSubErrorCodesToExceptions(true);
//
// Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("luke@fenetres.monkeymachine.eu","p!ssw0rd"));
//
// assertEquals(1, result.getAuthorities().size());
// assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
// }
} }