From 4df370b1007247f6e94bc48f4a16ad25b65272bd Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Tue, 18 Aug 2009 23:09:16 +0000 Subject: [PATCH] SEC-214: Add functionality to be able to use LDAP password policy request/response controls. Added PasswordPolicyAwareContextSource, ppolicy control implementations (from Sandbox) and modified BindAuthenticator to check for the presence of the response control, adding the control to the retured DirContextAdapter if appropriate. LdapUserDetailsImpl also contains the data for grace logins remaining and time till password expiry. Added OpenLDAP startup script with test data and integration test which operates against the data (must be run manually). --- ldap/openldaptest.ldif | 103 ++++++ ldap/pom.xml | 6 + ldap/run_slapd.sh | 7 + ldap/slapd.conf | 53 +++ .../DefaultSpringSecurityContextSource.java | 2 +- .../authentication/BindAuthenticator.java | 21 +- .../LdapAuthenticationProvider.java | 23 +- .../PasswordPolicyAwareContextSource.java | 87 +++++ .../ldap/ppolicy/PasswordPolicyControl.java | 88 +++++ .../PasswordPolicyControlExtractor.java | 38 ++ .../ppolicy/PasswordPolicyControlFactory.java | 47 +++ .../ldap/ppolicy/PasswordPolicyData.java | 12 + .../ppolicy/PasswordPolicyErrorStatus.java | 53 +++ .../ldap/ppolicy/PasswordPolicyException.java | 22 ++ .../PasswordPolicyResponseControl.java | 349 ++++++++++++++++++ .../ldap/userdetails/LdapUserDetailsImpl.java | 22 +- .../userdetails/LdapUserDetailsMapper.java | 21 +- .../ppolicy/OpenLDAPIntegrationTestSuite.java | 67 ++++ .../ppolicy/OpenLDAPIntegrationTestsuite.java | 67 ++++ .../PasswordPolicyResponseControlTests.java | 124 +++++++ ldap/template.mf | 4 +- 21 files changed, 1194 insertions(+), 22 deletions(-) create mode 100755 ldap/openldaptest.ldif create mode 100755 ldap/run_slapd.sh create mode 100755 ldap/slapd.conf create mode 100755 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java create mode 100755 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java create mode 100755 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java create mode 100755 ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java diff --git a/ldap/openldaptest.ldif b/ldap/openldaptest.ldif new file mode 100755 index 0000000000..78f0650c16 --- /dev/null +++ b/ldap/openldaptest.ldif @@ -0,0 +1,103 @@ +dn: dc=springsource,dc=com +objectClass: dcObject +objectClass: domain +dc: springsource + +dn: ou=users,dc=springsource,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=luke,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +cn: Luke +uid: luke +givenName: Luke +o: SpringSource +sn: Taylor +userPassword: password + +dn: ou=policies,dc=springsource,dc=com +objectClass: organizationalUnit +objectClass: top +ou: policies + +dn: cn=default,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: default +pwdAttribute: userPassword +pwdCheckQuality: 1 +pwdExpireWarning: 600000 +pwdFailureCountInterval: 0 +pwdGraceAuthNLimit: 100 +pwdInHistory: 50 +pwdLockout: FALSE +pwdLockoutDuration: 0 +pwdMaxAge: 5184000 +pwdMaxFailure: 3 +pwdMinAge: 0 +pwdMinLength: 8 +pwdMustChange: FALSE + +dn: cn=lockoutafter1,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: lockoutafter1 +pwdAttribute: userPassword +pwdCheckQuality: 1 +pwdFailureCountInterval: 0 +pwdGraceAuthNLimit: 2 +pwdInHistory: 3 +pwdLockout: TRUE +pwdLockoutDuration: 10 +pwdMaxFailure: 1 +pwdMinAge: 0 +pwdMinLength: 6 +pwdMustChange: TRUE + +dn: cn=expirein10,ou=policies,dc=springsource,dc=com +objectClass: device +objectClass: top +objectClass: pwdPolicy +cn: expirein10 +pwdAttribute: userPassword +pwdExpireWarning: 9999 +pwdGraceAuthNLimit: 5 +pwdMaxAge: 10000 +pwdInHistory: 3 +pwdLockout: FALSE +pwdMinLength: 6 +pwdMustChange: TRUE + + +dn: uid=expireme,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +uid: expireme +cn: Expired +givenName: Expired +o: SpringSource +sn: User +userPassword: password +pwdPolicySubentry: cn=expirein10,ou=policies,dc=springsource,dc=com + +dn: uid=lockme,ou=users,dc=springsource,dc=com +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: top +uid: lockme +cn: Expired +givenName: Expired +o: SpringSource +sn: User +userPassword: password +pwdPolicySubentry: cn=lockoutafter1,ou=policies,dc=springsource,dc=com \ No newline at end of file diff --git a/ldap/pom.xml b/ldap/pom.xml index 22a9253175..095939e923 100644 --- a/ldap/pom.xml +++ b/ldap/pom.xml @@ -23,6 +23,12 @@ commons-logging commons-logging + + ldapsdk + ldapsdk + 4.1 + true + org.apache.directory.server apacheds-core diff --git a/ldap/run_slapd.sh b/ldap/run_slapd.sh new file mode 100755 index 0000000000..5ea13b9ff7 --- /dev/null +++ b/ldap/run_slapd.sh @@ -0,0 +1,7 @@ +#! /bin/sh + +rm -Rf target/openldap +mkdir -p target/openldap +/opt/local/libexec/slapd -h ldap://localhost:22389 -d -1 -f slapd.conf & +sleep 2 +ldapadd -h localhost -p 22389 -D cn=admin,dc=springsource,dc=com -w password -x -f openldaptest.ldif \ No newline at end of file diff --git a/ldap/slapd.conf b/ldap/slapd.conf new file mode 100755 index 0000000000..e78aff339b --- /dev/null +++ b/ldap/slapd.conf @@ -0,0 +1,53 @@ +include /opt/local/etc/openldap/schema/core.schema +include /opt/local/etc/openldap/schema/cosine.schema +include /opt/local/etc/openldap/schema/inetorgperson.schema +include /opt/local/etc/openldap/schema/ppolicy.schema + + +pidfile ./target/slapd.pid +argsfile ./target/slapd.args + +# Load dynamic backend modules: +modulepath /usr/lib/openldap/modules +# moduleload back_ldap.la +# moduleload back_meta.la +# moduleload back_monitor.la +# moduleload back_perl.la + +disallow bind_anon +require authc + +access to dn.base="" + by * read + +database bdb +suffix "dc=springsource,dc=com" +checkpoint 1024 5 +cachesize 10000 +rootdn "cn=admin,dc=springsource,dc=com" + +rootpw password + +directory ./target/openldap + +index uid eq +index cn eq +index objectClass eq + +access to attrs=userpassword + by self =wx + by anonymous =x + by * none + +access to dn.subtree="ou=users,dc=qbe,dc=com" + by self write + by * read + + +overlay ppolicy +ppolicy_default "cn=default,ou=policies,dc=springsource,dc=com" +ppolicy_use_lockout +ppolicy_hash_cleartext + + + diff --git a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java index ca70de7f6a..b9a6f7d329 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java @@ -29,8 +29,8 @@ import org.springframework.util.Assert; * @since 2.0 */ public class DefaultSpringSecurityContextSource extends LdapContextSource { + protected final Log logger = LogFactory.getLog(getClass()); - private static final Log logger = LogFactory.getLog(DefaultSpringSecurityContextSource.class); private String rootDn; /** diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java index 277b363113..5e6079531c 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java @@ -29,6 +29,8 @@ import org.springframework.ldap.support.LdapUtils; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyControlExtractor; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -101,11 +103,22 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { logger.debug("Attempting to bind as " + fullDn); + DirContext ctx = null; try { - DirContext ctx = getContextSource().getContext(fullDn.toString(), password); + ctx = getContextSource().getContext(fullDn.toString(), password); + // Check for password policy control + PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx); + Attributes attrs = ctx.getAttributes(userDn, getUserAttributes()); - return new DirContextAdapter(attrs, new DistinguishedName(userDn), ctxSource.getBaseLdapPath()); + DirContextAdapter result = new DirContextAdapter(attrs, new DistinguishedName(userDn), + ctxSource.getBaseLdapPath()); + + if (ppolicy != null) { + result.setAttributeValue(ppolicy.getID(), ppolicy); + } + + return result; } catch (NamingException e) { // This will be thrown if an invalid user name is used and the method may // be called multiple times to try different names, so we trap the exception @@ -118,6 +131,8 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { } } catch (javax.naming.NamingException e) { throw LdapUtils.convertLdapException(e); + } finally { + LdapUtils.closeContext(ctx); } return null; @@ -125,7 +140,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { /** * Allows subclasses to inspect the exception thrown by an attempt to bind with a particular DN. - * The default implementation just reports the failure to the debug log. + * The default implementation just reports the failure to the debug logger. */ protected void handleBindException(String userDn, String username, Throwable cause) { if (logger.isDebugEnabled()) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java index 1e0a04fcc2..4ea6310d6f 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/LdapAuthenticationProvider.java @@ -17,9 +17,17 @@ package org.springframework.security.ldap.authentication; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.ldap.NamingException; +import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -28,21 +36,14 @@ import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.ldap.ppolicy.PasswordPolicyException; import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; -import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceAware; -import org.springframework.context.support.MessageSourceAccessor; -import org.springframework.ldap.NamingException; -import org.springframework.ldap.core.DirContextOperations; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** * An {@link org.springframework.security.authentication.AuthenticationProvider} implementation that authenticates @@ -124,7 +125,7 @@ import org.apache.commons.logging.LogFactory; * @author Luke Taylor * @version $Id$ * - * @see org.springframework.security.ldap.authentication.BindAuthenticator + * @see BindAuthenticator * @see DefaultLdapAuthoritiesPopulator */ public class LdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware { @@ -257,6 +258,10 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Messa UserDetails user = userDetailsContextMapper.mapUserFromContext(userData, username, extraAuthorities); return createSuccessfulAuthentication(userToken, user); + } catch (PasswordPolicyException ppe) { + // The only reason a ppolicy exception can occur during a bind is that the account is locked. + throw new LockedException(messages.getMessage(ppe.getStatus().getErrorCode(), + ppe.getStatus().getDefaultMessage())); } catch (UsernameNotFoundException notFound) { if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java new file mode 100755 index 0000000000..e2ffa7bba3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java @@ -0,0 +1,87 @@ +package org.springframework.security.ldap.ppolicy; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.directory.DirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; + +import org.springframework.ldap.support.LdapUtils; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; + + +/** + * Extended version of the DefaultSpringSecurityContextSource which adds support for + * the use of {@link PasswordPolicyControl} to make use of user account data stored in the directory. + *

+ * When binding with specific username (not the userDn) property it will connect + * first as the userDn, then reconnect as the user in order to retrieve any password-policy control + * sent with the response, even if an exception occurs. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource { + + public PasswordPolicyAwareContextSource(String providerUrl) { + super(providerUrl); + } + + @Override + public DirContext getContext(String principal, String credentials) throws PasswordPolicyException { + if (principal.equals(userDn)) { + return super.getContext(principal, credentials); + } + + final boolean debug = logger.isDebugEnabled(); + + if (debug) { + logger.debug("Binding as '" + userDn + "', prior to reconnect as user '" + principal + "'" ); + } + + // First bind as manager user before rebinding as the specific principal. + LdapContext ctx = (LdapContext) super.getContext(userDn, password); + + Control[] rctls = { new PasswordPolicyControl(false) }; + + try { + ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal ); + ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + ctx.reconnect(rctls); + } catch(javax.naming.NamingException ne) { + PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); + if (debug) { + logger.debug("Failed to obtain context", ne); + logger.debug("Pasword policy response: " + ctrl); + } + + LdapUtils.closeContext(ctx); + + if (ctrl != null) { + if (ctrl.isLocked()) { + throw new PasswordPolicyException(ctrl.getErrorStatus()); + } + } + + throw LdapUtils.convertLdapException(ne); + } + + if (debug) { + logger.debug("PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx)); + } + + return ctx; + } + + @Override + @SuppressWarnings("unchecked") + protected Hashtable getAuthenticatedEnv(String principal, String credentials) { + Hashtable env = super.getAuthenticatedEnv(principal, credentials); + + env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); + + return env; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java new file mode 100755 index 0000000000..f1d524eab3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java @@ -0,0 +1,88 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.ppolicy; + +import javax.naming.ldap.Control; + + +/** + * + * A Password Policy request control. + *

+ * Based on the information in the corresponding internet draft on LDAP password policy. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + * + * @see PasswordPolicyResponseControl + * @see Password Policy for LDAP + * Directories + */ +public class PasswordPolicyControl implements Control { + //~ Static fields/initializers ===================================================================================== + + /** OID of the Password Policy Control */ + public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1"; + + //~ Instance fields ================================================================================================ + + private boolean critical; + + //~ Constructors =================================================================================================== + + /** + * Creates a non-critical (request) control. + */ + public PasswordPolicyControl() { + this(Control.NONCRITICAL); + } + + /** + * Creates a (request) control. + * + * @param critical indicates whether the control is critical for the client + */ + public PasswordPolicyControl(boolean critical) { + this.critical = critical; + } + + //~ Methods ======================================================================================================== + + /** + * Retrieves the ASN.1 BER encoded value of the LDAP control. The request value for this control is always + * empty. + * + * @return always null + */ + public byte[] getEncodedValue() { + return null; + } + + /** + * Returns the OID of the Password Policy Control ("1.3.6.1.4.1.42.2.27.8.5.1"). + */ + public String getID() { + return OID; + } + + /** + * Returns whether the control is critical for the client. + */ + public boolean isCritical() { + return critical; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java new file mode 100644 index 0000000000..5eddfd2c31 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java @@ -0,0 +1,38 @@ +package org.springframework.security.ldap.ppolicy; + +import javax.naming.directory.DirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Obtains the PasswordPolicyControl from a context for use by other classes. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyControlExtractor { + private static final Log logger = LogFactory.getLog(PasswordPolicyControlExtractor.class); + + public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) { + LdapContext ctx = (LdapContext) dirCtx; + Control[] ctrls = null; + try { + ctrls = ctx.getResponseControls(); + } catch (javax.naming.NamingException e) { + logger.error("Failed to obtain response controls", e); + } + + for (int i = 0; ctrls != null && i < ctrls.length; i++) { + if (ctrls[i] instanceof PasswordPolicyResponseControl) { + return (PasswordPolicyResponseControl) ctrls[i]; + } + } + + return null; + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java new file mode 100755 index 0000000000..2435a68673 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java @@ -0,0 +1,47 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.ppolicy; + +import javax.naming.ldap.Control; +import javax.naming.ldap.ControlFactory; + + +/** + * Transforms a control object to a PasswordPolicyResponseControl object, if appropriate. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + */ +public class PasswordPolicyControlFactory extends ControlFactory { + //~ Methods ======================================================================================================== + + /** + * Creates an instance of PasswordPolicyResponseControl if the passed control is a response control of this + * type. Attributes of the result are filled with the correct values (e.g. error code). + * + * @param ctl the control the check + * + * @return a response control of type PasswordPolicyResponseControl, or null + */ + public Control getControlInstance(Control ctl) { + if (ctl.getID().equals(PasswordPolicyControl.OID)) { + return new PasswordPolicyResponseControl(ctl.getEncodedValue()); + } + + return null; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java new file mode 100644 index 0000000000..887b65a970 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java @@ -0,0 +1,12 @@ +package org.springframework.security.ldap.ppolicy; + +/** + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public interface PasswordPolicyData { + int getTimeBeforeExpiration(); + + int getGraceLoginsRemaining(); +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java new file mode 100644 index 0000000000..a727270d5e --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java @@ -0,0 +1,53 @@ +package org.springframework.security.ldap.ppolicy; + + +/** + * Defines status codes for use with PasswordPolicyException, with error codes (for message source lookup) and default + * messages. + * + *

+ *    PasswordPolicyResponseValue ::= SEQUENCE {
+ *        warning [0] CHOICE {
+ *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt)
+ *        } OPTIONAL,
+ *        error   [1] ENUMERATED {
+ *           passwordExpired             (0),     accountLocked               (1),
+ *           changeAfterReset            (2),     passwordModNotAllowed       (3),
+ *           mustSupplyOldPassword       (4),     insufficientPasswordQuality (5),
+ *           passwordTooShort            (6),     passwordTooYoung            (7),
+ *           passwordInHistory           (8)
+ *        } OPTIONAL
+ *    }
+ *
+ * + * @author Luke Taylor + * @since 3.0 + */ +public enum PasswordPolicyErrorStatus { + PASSWORD_EXPIRED ("ppolicy.expired", "Your password has expired"), + ACCOUNT_LOCKED ("ppolicy.locked", "Account is locked"), + CHANGE_AFTER_RESET ("ppolicy.change.after.reset", "Your password must be changed after being reset"), + PASSWORD_MOD_NOT_ALLOWED ("ppolicy.mod.not.allowed", "Password cannot be changed"), + MUST_SUPPLY_OLD_PASSWORD ("ppolicy.must.supply.old.password", "The old password must be supplied"), + INSUFFICIENT_PASSWORD_QUALITY ("ppolicy.insufficient.password.quality", "The supplied password is of insufficient quality"), + PASSWORD_TOO_SHORT ("ppolicy.password.too.short", "The supplied password is too short"), + PASSWORD_TOO_YOUNG ("ppolicy.password.too.young", "Your password was changed too recently to be changed again"), + PASSWORD_IN_HISTORY ("ppolicy.password.in.history", "The supplied password has already been used"); + + private String errorCode; + private String defaultMessage; + + private PasswordPolicyErrorStatus(String errorCode, String defaultMessage) { + this.errorCode = errorCode; + this.defaultMessage = defaultMessage; + } + + public String getErrorCode() { + return errorCode; + } + + public String getDefaultMessage() { + return defaultMessage; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java new file mode 100644 index 0000000000..3964232314 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java @@ -0,0 +1,22 @@ +package org.springframework.security.ldap.ppolicy; + +/** + * Generic exception raised by the ppolicy package. + *

+ * The status property should be checked for more detail on the cause of the exception. + * + * @author Luke Taylor + * @since 3.0 + */ +public class PasswordPolicyException extends RuntimeException { + private PasswordPolicyErrorStatus status; + + public PasswordPolicyException(PasswordPolicyErrorStatus status) { + super(status.getDefaultMessage()); + this.status = status; + } + + public PasswordPolicyErrorStatus getStatus() { + return status; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java new file mode 100755 index 0000000000..4bb7de0aa8 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -0,0 +1,349 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.ppolicy; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import netscape.ldap.ber.stream.BERChoice; +import netscape.ldap.ber.stream.BERElement; +import netscape.ldap.ber.stream.BEREnumerated; +import netscape.ldap.ber.stream.BERInteger; +import netscape.ldap.ber.stream.BERIntegral; +import netscape.ldap.ber.stream.BERSequence; +import netscape.ldap.ber.stream.BERTag; +import netscape.ldap.ber.stream.BERTagDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataRetrievalFailureException; + + +/** + * Represents the response control received when a PasswordPolicyControl is used when binding to a + * directory. Currently tested with the OpenLDAP 2.3.19 implementation of the LDAP Password Policy Draft. It extends + * the request control with the control specific data. This is accomplished by the properties timeBeforeExpiration, + * graceLoginsRemaining. + *

+ * + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControl.java,v 1.4 2009/03/04 07:25:07 itslxt Exp $ + * + * @see org.springframework.security.ldap.ppolicy.PasswordPolicyControl + * @see Stefan Zoerner's IBM developerworks + * article on LDAP controls. + */ +public class PasswordPolicyResponseControl extends PasswordPolicyControl { + //~ Static fields/initializers ===================================================================================== + + private static final Log logger = LogFactory.getLog(PasswordPolicyResponseControl.class); + + //~ Instance fields ================================================================================================ + + private byte[] encodedValue; + + private PasswordPolicyErrorStatus errorStatus; + + private int graceLoginsRemaining = Integer.MAX_VALUE; + private int timeBeforeExpiration = Integer.MAX_VALUE; + + //~ Constructors =================================================================================================== + + /** + * Decodes the Ber encoded control data. The ASN.1 value of the control data is:

+     *    PasswordPolicyResponseValue ::= SEQUENCE {       warning [0] CHOICE {
+     *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+     *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL,       error   [1] ENUMERATED {
+     *           passwordExpired             (0),          accountLocked               (1),
+     *           changeAfterReset            (2),          passwordModNotAllowed       (3),
+     *           mustSupplyOldPassword       (4),          insufficientPasswordQuality (5),
+     *           passwordTooShort            (6),          passwordTooYoung            (7),
+     *           passwordInHistory           (8) } OPTIONAL }
+ * + */ + public PasswordPolicyResponseControl(byte[] encodedValue) { + this.encodedValue = encodedValue; + + //PPolicyDecoder decoder = new JLdapDecoder(); + PPolicyDecoder decoder = new NetscapeDecoder(); + + try { + decoder.decode(); + } catch (IOException e) { + throw new DataRetrievalFailureException("Failed to parse control value", e); + } + } + + //~ Methods ======================================================================================================== + + /** + * Returns the unchanged value of the response control. Returns the unchanged value of the response + * control as byte array. + */ + public byte[] getEncodedValue() { + return encodedValue; + } + + public PasswordPolicyErrorStatus getErrorStatus() { + return errorStatus; + } + + /** + * Returns the graceLoginsRemaining. + * + * @return Returns the graceLoginsRemaining. + */ + public int getGraceLoginsRemaining() { + return graceLoginsRemaining; + } + + /** + * Returns the timeBeforeExpiration. + * + * @return Returns the time before expiration in seconds + */ + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + /** + * Checks whether an error is present. + * + * @return true, if an error is present + */ + public boolean hasError() { + return errorStatus != null; + } + + /** + * Checks whether a warning is present. + * + * @return true, if a warning is present + */ + public boolean hasWarning() { + return (graceLoginsRemaining != Integer.MAX_VALUE) || (timeBeforeExpiration != Integer.MAX_VALUE); + } + + public boolean isExpired() { + return errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED; + } + + public boolean isChangeAfterReset() { + return errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET; + } + + public boolean isUsingGraceLogins() { + return graceLoginsRemaining < Integer.MAX_VALUE; + } + + /** + * Determines whether an account locked error has been returned. + * + * @return true if the account is locked. + */ + public boolean isLocked() { + return errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED; + } + + /** + * Create a textual representation containing error and warning messages, if any are present. + * + * @return error and warning messages + */ + public String toString() { + StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl"); + + if (hasError()) { + sb.append(", error: ").append(errorStatus.getDefaultMessage()); + } + + if (graceLoginsRemaining != Integer.MAX_VALUE) { + sb.append(", warning: ").append(graceLoginsRemaining).append(" grace logins remain"); + } + + if (timeBeforeExpiration != Integer.MAX_VALUE) { + sb.append(", warning: time before expiration is ").append(timeBeforeExpiration); + } + + if (!hasError() && !hasWarning()) { + sb.append(" (no error, no warning)"); + } + + return sb.toString(); + } + + //~ Inner Interfaces =============================================================================================== + + private interface PPolicyDecoder { + void decode() throws IOException; + } + + //~ Inner Classes ================================================================================================== + + /** + * Decoder based on Netscape ldapsdk library + */ + private class NetscapeDecoder implements PPolicyDecoder { + public void decode() throws IOException { + int[] bread = {0}; + BERSequence seq = (BERSequence) BERElement.getElement(new SpecificTagDecoder(), + new ByteArrayInputStream(encodedValue), bread); + + int size = seq.size(); + + if (logger.isDebugEnabled()) { + logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size + " elements"); + } + + for (int i = 0; i < seq.size(); i++) { + BERTag elt = (BERTag) seq.elementAt(i); + + int tag = elt.getTag() & 0x1F; + + if (tag == 0) { + BERChoice warning = (BERChoice) elt.getValue(); + + BERTag content = (BERTag) warning.getValue(); + int value = ((BERInteger) content.getValue()).getValue(); + + if ((content.getTag() & 0x1F) == 0) { + timeBeforeExpiration = value; + } else { + graceLoginsRemaining = value; + } + } else if (tag == 1) { + BERIntegral error = (BERIntegral) elt.getValue(); + errorStatus = PasswordPolicyErrorStatus.values()[error.getValue()]; + } + } + } + + class SpecificTagDecoder extends BERTagDecoder { + /** Allows us to remember which of the two options we're decoding */ + private Boolean inChoice = null; + + public BERElement getElement(BERTagDecoder decoder, int tag, InputStream stream, int[] bytesRead, + boolean[] implicit) throws IOException { + tag &= 0x1F; + implicit[0] = false; + + if (tag == 0) { + // Either the choice or the time before expiry within it + if (inChoice == null) { + setInChoice(true); + + // Read the choice length from the stream (ignored) + BERElement.readLengthOctets(stream, bytesRead); + + int[] componentLength = new int[1]; + BERElement choice = new BERChoice(decoder, stream, componentLength); + bytesRead[0] += componentLength[0]; + + // inChoice = null; + return choice; + } else { + // Must be time before expiry + return new BERInteger(stream, bytesRead); + } + } else if (tag == 1) { + // Either the graceLogins or the error enumeration. + if (inChoice == null) { + // The enumeration + setInChoice(false); + + return new BEREnumerated(stream, bytesRead); + } else { + if (inChoice.booleanValue()) { + // graceLogins + return new BERInteger(stream, bytesRead); + } + } + } + + throw new DataRetrievalFailureException("Unexpected tag " + tag); + } + + private void setInChoice(boolean inChoice) { + this.inChoice = new Boolean(inChoice); + } + } + } + +/** Decoder based on the OpenLDAP/Novell JLDAP library */ + +// private class JLdapDecoder implements PPolicyDecoder { +// +// public void decode() throws IOException { +// +// LBERDecoder decoder = new LBERDecoder(); +// +// ASN1Sequence seq = (ASN1Sequence)decoder.decode(encodedValue); +// +// if(seq == null) { +// +// } +// +// int size = seq.size(); +// +// if(logger.isDebugEnabled()) { +// logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + +// size + " elements"); +// } +// +// for(int i=0; i < size; i++) { +// +// ASN1Tagged taggedObject = (ASN1Tagged)seq.get(i); +// +// int tag = taggedObject.getIdentifier().getTag(); +// +// ASN1OctetString value = (ASN1OctetString)taggedObject.taggedValue(); +// byte[] content = value.byteValue(); +// +// if(tag == 0) { +// parseWarning(content, decoder); +// +// } else if(tag == 1) { +// // Error: set the code to the value +// errorCode = content[0]; +// } +// } +// } +// +// private void parseWarning(byte[] content, LBERDecoder decoder) { +// // It's the warning (choice). Parse the number and set either the +// // expiry time or number of logins remaining. +// ASN1Tagged taggedObject = (ASN1Tagged)decoder.decode(content); +// int contentTag = taggedObject.getIdentifier().getTag(); +// content = ((ASN1OctetString)taggedObject.taggedValue()).byteValue(); +// int number; +// +// try { +// number = ((Long)decoder.decodeNumeric(new ByteArrayInputStream(content), content.length)).intValue(); +// } catch(IOException e) { +// throw new LdapDataAccessException("Failed to parse number ", e); +// } +// +// if(contentTag == 0) { +// timeBeforeExpiration = number; +// } else if (contentTag == 1) { +// graceLoginsRemaining = number; +// } +// } +// } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java index 806b7a031e..923ebc7a02 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java @@ -23,6 +23,7 @@ import javax.naming.Name; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.ldap.ppolicy.PasswordPolicyData; import org.springframework.util.Assert; @@ -40,7 +41,7 @@ import org.springframework.util.Assert; * @author Luke Taylor * @version $Id$ */ -public class LdapUserDetailsImpl implements LdapUserDetails { +public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData { //~ Instance fields ================================================================================================ @@ -52,6 +53,9 @@ public class LdapUserDetailsImpl implements LdapUserDetails { private boolean accountNonLocked = true; private boolean credentialsNonExpired = true; private boolean enabled = true; + // PPolicy data + private int timeBeforeExpiration = Integer.MAX_VALUE; + private int graceLoginsRemaining = Integer.MAX_VALUE; //~ Constructors =================================================================================================== @@ -91,6 +95,14 @@ public class LdapUserDetailsImpl implements LdapUserDetails { return enabled; } + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + public int getGraceLoginsRemaining() { + return graceLoginsRemaining; + } + public String toString() { StringBuffer sb = new StringBuffer(); sb.append(super.toString()).append(": "); @@ -217,5 +229,13 @@ public class LdapUserDetailsImpl implements LdapUserDetails { public void setUsername(String username) { instance.username = username; } + + public void setTimeBeforeExpiration(int timeBeforeExpiration) { + instance.timeBeforeExpiration = timeBeforeExpiration; + } + + public void setGraceLoginsRemaining(int graceLoginsRemaining) { + instance.graceLoginsRemaining = graceLoginsRemaining; + } } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java index 36b8cf7b4e..932e6f0921 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java @@ -17,16 +17,16 @@ package org.springframework.security.ldap.userdetails; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.UserDetails; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - +import org.springframework.security.ldap.ppolicy.PasswordPolicyControl; +import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl; import org.springframework.util.Assert; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DirContextOperations; /** @@ -86,6 +86,15 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper { essence.addAuthority(authorities.get(i)); } + // Check for PPolicy data + + PasswordPolicyResponseControl ppolicy = (PasswordPolicyResponseControl) ctx.getObjectAttribute(PasswordPolicyControl.OID); + + if (ppolicy != null) { + essence.setTimeBeforeExpiration(ppolicy.getTimeBeforeExpiration()); + essence.setGraceLoginsRemaining(ppolicy.getGraceLoginsRemaining()); + } + return essence.createUserDetails(); } diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java new file mode 100644 index 0000000000..2b78c21e1e --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java @@ -0,0 +1,67 @@ +package org.springframework.security.ldap.ppolicy; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * Test cases which run against an OpenLDAP server. + *

+ * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java new file mode 100644 index 0000000000..2b78c21e1e --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java @@ -0,0 +1,67 @@ +package org.springframework.security.ldap.ppolicy; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * Test cases which run against an OpenLDAP server. + *

+ * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java new file mode 100644 index 0000000000..bcb27c0362 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java @@ -0,0 +1,124 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * 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.ppolicy; + +import junit.framework.TestCase; + +/** + * Tests for PasswordPolicyResponse. + * + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControlTests.java 2217 2007-10-27 00:45:30Z luke_t $ + */ +public class PasswordPolicyResponseControlTests extends TestCase { + //~ Methods ======================================================================================================== + + /** + * Useful method for obtaining data from a server for use in tests + */ +// public void testAgainstServer() throws Exception { +// Hashtable env = new Hashtable(); +// env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); +// env.put(Context.PROVIDER_URL, "ldap://gorille:389/"); +// env.put(Context.SECURITY_AUTHENTICATION, "simple"); +// env.put(Context.SECURITY_PRINCIPAL, "cn=manager,dc=security,dc=org"); +// env.put(Context.SECURITY_CREDENTIALS, "security"); +// env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); +// +// InitialLdapContext ctx = new InitialLdapContext(env, null); +// +// Control[] rctls = { new PasswordPolicyControl(false) }; +// +// ctx.setRequestControls(rctls); +// +// try { +// ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=bob,ou=people,dc=security,dc=org" ); +// ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "bobspassword"); +// Object o = ctx.lookup(""); +// +// System.out.println(o); +// +// } catch(NamingException ne) { +// // Ok. +// System.err.println(ne); +// } +// +// PasswordPolicyResponseControl ctrl = getPPolicyResponseCtl(ctx); +// System.out.println(ctrl); +// +// assertNotNull(ctrl); +// +// //com.sun.jndi.ldap.LdapPoolManager.showStats(System.out); +// } + + +// private PasswordPolicyResponseControl getPPolicyResponseCtl(InitialLdapContext ctx) throws NamingException { +// Control[] ctrls = ctx.getResponseControls(); +// +// for (int i = 0; ctrls != null && i < ctrls.length; i++) { +// if (ctrls[i] instanceof PasswordPolicyResponseControl) { +// return (PasswordPolicyResponseControl) ctrls[i]; +// } +// } +// +// return null; +// } + + public void testOpenLDAP33SecondsTillPasswordExpiryCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA0, 0x1, 0x21}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(33, ctrl.getTimeBeforeExpiration()); + } + + public void testOpenLDAP496GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x06, (byte) 0xA0, 0x04, (byte) 0xA1, 0x02, 0x01, (byte) 0xF0}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(496, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAP5GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA1, 0x01, 0x05}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(5, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAPAccountLockedCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x01}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isLocked()); + assertFalse(ctrl.hasWarning()); + } + + public void testOpenLDAPPasswordExpiredCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x00}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isExpired()); + assertFalse(ctrl.hasWarning()); + } +} diff --git a/ldap/template.mf b/ldap/template.mf index 64a2d80fe6..7bca39354e 100644 --- a/ldap/template.mf +++ b/ldap/template.mf @@ -19,5 +19,5 @@ Import-Template: org.springframework.core.io.*;version="[3.0.0, 3.1.0)", org.springframework.dao.*;version="[3.0.0, 3.1.0)";resolution:=optional, org.springframework.util.*;version="[3.0.0, 3.1.0)", - javax.naming.*;version="0";resolution:=optional - \ No newline at end of file + javax.naming.*;version="0";resolution:=optional, + netscape.ldap.ber.stream;version="[4.1, 5.0)";resolution:=optional \ No newline at end of file