@ -2,42 +2,84 @@ package org.springframework.security.ldap.authentication.ad;
@@ -2,42 +2,84 @@ package org.springframework.security.ldap.authentication.ad;
import org.springframework.ldap.core.DirContextOperations ;
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.CredentialsExpiredException ;
import org.springframework.security.authentication.DisabledException ;
import org.springframework.security.authentication.LockedException ;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken ;
import org.springframework.security.core.GrantedAuthority ;
import org.springframework.security.core.authority.AuthorityUtils ;
import org.springframework.security.core.authority.SimpleGrantedAuthority ;
import org.springframework.security.ldap.LdapUtils ;
import org.springframework.security.ldap.SpringSecurityLdapTemplate ;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider ;
import org.springframework.util.Assert ;
import org.springframework.util.StringUtils ;
import javax.naming.AuthenticationException ;
import javax.naming.Context ;
import javax.naming.NamingException ;
import javax.naming.OperationNotSupportedException ;
import javax.naming.directory.SearchControls ;
import javax.naming.ldap.InitialLdapContext ;
import javax.naming.ldap.LdapContext ;
import java.util.* ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
/ * *
* Specialized LDAP authentication provider which uses Active Directory configuration conventions .
* < p >
* It will authenticate using the Active Directory { @code userPrincipalName } ( in the form { @code username @domain } ) .
* If the { @code usernameIncludesDomain } property is set to { @code true } , it is assumed that the user types in the
* full value , including the domain . Otherwise ( the default ) , the { @code userPrincipalName } will be built from the
* username supplied in the authentication request .
* It will authenticate using the Active Directory
* < a href = "http://msdn.microsoft.com/en-us/library/ms680857%28VS.85%29.aspx" > { @code userPrincipalName } < / a >
* ( in the form { @code username @domain } ) . If the username does not already end with the domain name , the
* { @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 >
* 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
* @since 3 . 1
* /
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 rootDn ;
private final String url ;
private boolean usernameIncludesDomain = false ;
private boolean convertSubErrorCodesToExceptions ;
/ * *
* @param domain the domain for which authentication should take place
@ -52,7 +94,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@@ -52,7 +94,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
* /
public ActiveDirectoryLdapAuthenticationProvider ( String domain , String url ) {
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 ;
rootDn = this . domain = = null ? null : rootDnFromDomain ( this . domain ) ;
}
@ -68,8 +110,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@@ -68,8 +110,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return searchForUser ( ctx , username ) ;
} catch ( NamingException e ) {
logger . error ( "Failed to locate directory entry for authentication user: " + username , e ) ;
throw authenticationFailure ( ) ;
logger . error ( "Failed to locate directory entry for authenticated user: " + username , e ) ;
throw badCredentials ( ) ;
} finally {
LdapUtils . closeContext ( ctx ) ;
}
@ -108,7 +150,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@@ -108,7 +150,8 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
Hashtable < String , String > env = new Hashtable < String , String > ( ) ;
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 . SECURITY_CREDENTIALS , password ) ;
env . put ( Context . INITIAL_CONTEXT_FACTORY , "com.sun.jndi.ldap.LdapCtxFactory" ) ;
@ -116,12 +159,84 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@@ -116,12 +159,84 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
try {
return new InitialLdapContext ( env , null ) ;
} catch ( NamingException e ) {
logger . debug ( "Authentication failed" , e ) ;
throw authenticationFailure ( ) ;
if ( ( e instanceof AuthenticationException ) | | ( e instanceof OperationNotSupportedException ) ) {
handleBindException ( bindPrincipal , e ) ;
throw badCredentials ( ) ;
} else {
throw LdapUtils . convertLdapException ( e ) ;
}
}
}
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" ) ;
}
}
private BadCredentialsException authenticationFailure ( ) {
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 (
"LdapAuthenticationProvider.badCredentials" , "Bad credentials" ) ) ;
}
@ -158,18 +273,27 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
@@ -158,18 +273,27 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
return root . toString ( ) ;
}
private String createBindPrincipal ( String username ) {
if ( usernameIncludesDomain | | domain = = null ) {
String createBindPrincipal ( String username ) {
if ( domain = = null | | username . toLowerCase ( ) . endsWith ( domain ) ) {
return username ;
}
return username + "@" + domain ;
}
public void setUsernameIncludesDomain ( boolean usernameIncludesDomain ) {
Assert . isTrue ( domain ! = null | | usernameIncludesDomain ,
"If the domain name is not included in the username, a domain must be set in the constructor" ) ;
this . usernameIncludesDomain = usernameIncludesDomain ;
/ * *
* By default , a failed authentication ( LDAP error 49 ) will result in a { @code BadCredentialsException } .
* < p >
* 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 ;
}
}