diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryAuthenticationException.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryAuthenticationException.java new file mode 100644 index 0000000000..54a5f68e41 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryAuthenticationException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.springframework.security.ldap.authentication.ad; + +import org.springframework.security.core.AuthenticationException; + +/** + *
+ * Thrown as a translation of an {@link javax.naming.AuthenticationException} when attempting to authenticate against + * Active Directory using {@link ActiveDirectoryLdapAuthenticationProvider}. Typically this error is wrapped by an + * {@link AuthenticationException} since it does not provide a user friendly message. When wrapped, the original + * Exception can be caught and {@link ActiveDirectoryAuthenticationException} can be accessed using + * {@link AuthenticationException#getCause()} for custom error handling. + *
+ *+ * The {@link #getDataCode()} will return the error code associated with the data portion of the error message. For + * example, the following error message would return 773 for {@link #getDataCode()}. + *
+ * + *+ * javax.naming.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 775, vece ] + *+ * + * @author Rob Winch + */ +@SuppressWarnings("serial") +public final class ActiveDirectoryAuthenticationException extends AuthenticationException { + private final String dataCode; + + ActiveDirectoryAuthenticationException(String dataCode, String message, Throwable cause) { + super(message, cause); + this.dataCode = dataCode; + } + + public String getDataCode() { + return dataCode; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java index 054d7cb8cb..89eba6015c 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java @@ -1,3 +1,15 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ package org.springframework.security.ldap.authentication.ad; import org.springframework.ldap.core.DirContextOperations; @@ -61,6 +73,7 @@ import java.util.regex.Pattern; * {@code true}, the codes will also be used to control the exception raised. * * @author Luke Taylor + * @author Rob Winch * @since 3.1 */ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { @@ -115,7 +128,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda } catch (NamingException e) { logger.error("Failed to locate directory entry for authenticated user: " + username, e); - throw badCredentials(); + throw badCredentials(e); } finally { LdapUtils.closeContext(ctx); } @@ -165,7 +178,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda } catch (NamingException e) { if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) { handleBindException(bindPrincipal, e); - throw badCredentials(); + throw badCredentials(e); } else { throw LdapUtils.convertLdapException(e); } @@ -183,7 +196,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode)); if (convertSubErrorCodesToExceptions) { - raiseExceptionForErrorCode(subErrorCode); + raiseExceptionForErrorCode(subErrorCode, exception); } } else { logger.debug("Failed to locate AD-specific sub-error code in message"); @@ -200,20 +213,24 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda return -1; } - void raiseExceptionForErrorCode(int code) { + void raiseExceptionForErrorCode(int code, NamingException exception) { + String hexString = Integer.toHexString(code); + Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception); switch (code) { case PASSWORD_EXPIRED: throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired", - "User credentials have expired")); + "User credentials have expired"), cause); case ACCOUNT_DISABLED: throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled", - "User is disabled")); + "User is disabled"), cause); case ACCOUNT_EXPIRED: throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired", - "User account has expired")); + "User account has expired"), cause); case ACCOUNT_LOCKED: throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked", - "User account is locked")); + "User account is locked"), cause); + default: + throw badCredentials(cause); } } @@ -245,6 +262,10 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda "LdapAuthenticationProvider.badCredentials", "Bad credentials")); } + private BadCredentialsException badCredentials(Throwable cause) { + return (BadCredentialsException) badCredentials().initCause(cause); + } + private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException { SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java index 22a77eacf2..39250b4039 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java @@ -1,10 +1,27 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ package org.springframework.security.ldap.authentication.ad; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.ContextFactory; +import org.hamcrest.BaseMatcher; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.*; +import org.junit.rules.ExpectedException; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DistinguishedName; import org.springframework.security.authentication.AccountExpiredException; @@ -29,8 +46,12 @@ import java.util.*; /** * @author Luke Taylor + * @author Rob Winch */ public class ActiveDirectoryLdapAuthenticationProviderTests { + @Rule + public ExpectedException thrown = ExpectedException.none(); + ActiveDirectoryLdapAuthenticationProvider provider; UsernamePasswordAuthenticationToken joe = new UsernamePasswordAuthenticationToken("joe", "password"); @@ -127,10 +148,30 @@ public class ActiveDirectoryLdapAuthenticationProviderTests { provider.authenticate(joe); } - @Test(expected = BadCredentialsException.class) + @Test public void passwordNeedsResetIsCorrectlyMapped() { - provider.contextFactory = createContextFactoryThrowing(new AuthenticationException(msg + "773, xxxx]")); + final String dataCode = "773"; + provider.contextFactory = createContextFactoryThrowing(new AuthenticationException(msg + dataCode+", xxxx]")); provider.setConvertSubErrorCodesToExceptions(true); + + thrown.expect(BadCredentialsException.class); + thrown.expect(new BaseMatcher