mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 09:12:14 +00:00
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:
parent
48988bde84
commit
4df370b100
103
ldap/openldaptest.ldif
Executable file
103
ldap/openldaptest.ldif
Executable 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
|
@ -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
7
ldap/run_slapd.sh
Executable 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
53
ldap/slapd.conf
Executable 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
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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()) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user