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).

This commit is contained in:
Luke Taylor 2009-08-18 23:09:16 +00:00
parent 48988bde84
commit 4df370b100
21 changed files with 1194 additions and 22 deletions

103
ldap/openldaptest.ldif Executable file
View File

@ -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

View File

@ -23,6 +23,12 @@
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>
<dependency>
<groupId>ldapsdk</groupId>
<artifactId>ldapsdk</artifactId>
<version>4.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>

7
ldap/run_slapd.sh Executable file
View File

@ -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

53
ldap/slapd.conf Executable file
View File

@ -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

View File

@ -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;
/**

View File

@ -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()) {

View File

@ -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(

View File

@ -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 <tt>DefaultSpringSecurityContextSource</tt> which adds support for
* the use of {@link PasswordPolicyControl} to make use of user account data stored in the directory.
* <p>
* When binding with specific username (not the <tt>userDn</tt>) 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;
}
}

View File

@ -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.
* <p>
* Based on the information in the corresponding internet draft on LDAP password policy.
*
* @author Stefan Zoerner
* @author Luke Taylor
* @version $Id$
*
* @see PasswordPolicyResponseControl
* @see <a href="http://www.ietf.org/internet-drafts/draft-behera-ldap-password-policy-09.txt">Password Policy for LDAP
* Directories</a>
*/
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;
}
}

View File

@ -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 <tt>PasswordPolicyControl</tt> 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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -0,0 +1,53 @@
package org.springframework.security.ldap.ppolicy;
/**
* Defines status codes for use with <tt>PasswordPolicyException</tt>, with error codes (for message source lookup) and default
* messages.
*
* <pre>
* 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
* }
*</pre>
*
* @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;
}
}

View File

@ -0,0 +1,22 @@
package org.springframework.security.ldap.ppolicy;
/**
* Generic exception raised by the ppolicy package.
* <p>
* The <tt>status</tt> 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;
}
}

View File

@ -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 <tt>PasswordPolicyControl</tt> 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 <tt>timeBeforeExpiration</tt>,
* <tt>graceLoginsRemaining</tt>.
* <p>
*
*
* @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 <a href="http://www.ibm.com/developerworks/tivoli/library/t-ldap-controls/">Stefan Zoerner's IBM developerworks
* article on LDAP controls.</a>
*/
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:<pre>
* 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 }</pre>
*
*/
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;
// }
// }
// }
}

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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 <tt>PasswordPolicyResponse</tt>.
*
* @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());
}
}

View File

@ -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
javax.naming.*;version="0";resolution:=optional,
netscape.ldap.ber.stream;version="[4.1, 5.0)";resolution:=optional