+ * When binding with specific username (not the userDn) property it will connect + * first as the userDn, then reconnect as the user in order to retrieve any password-policy control + * sent with the response, even if an exception occurs. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource { + + public PasswordPolicyAwareContextSource(String providerUrl) { + super(providerUrl); + } + + @Override + public DirContext getContext(String principal, String credentials) throws PasswordPolicyException { + if (principal.equals(userDn)) { + return super.getContext(principal, credentials); + } + + final boolean debug = logger.isDebugEnabled(); + + if (debug) { + logger.debug("Binding as '" + userDn + "', prior to reconnect as user '" + principal + "'" ); + } + + // First bind as manager user before rebinding as the specific principal. + LdapContext ctx = (LdapContext) super.getContext(userDn, password); + + Control[] rctls = { new PasswordPolicyControl(false) }; + + try { + ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal ); + ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + ctx.reconnect(rctls); + } catch(javax.naming.NamingException ne) { + PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); + if (debug) { + logger.debug("Failed to obtain context", ne); + logger.debug("Pasword policy response: " + ctrl); + } + + LdapUtils.closeContext(ctx); + + if (ctrl != null) { + if (ctrl.isLocked()) { + throw new PasswordPolicyException(ctrl.getErrorStatus()); + } + } + + throw LdapUtils.convertLdapException(ne); + } + + if (debug) { + logger.debug("PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx)); + } + + return ctx; + } + + @Override + @SuppressWarnings("unchecked") + protected Hashtable getAuthenticatedEnv(String principal, String credentials) { + Hashtable env = super.getAuthenticatedEnv(principal, credentials); + + env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); + + return env; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java new file mode 100755 index 0000000000..f1d524eab3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControl.java @@ -0,0 +1,88 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.ppolicy; + +import javax.naming.ldap.Control; + + +/** + * + * A Password Policy request control. + *
+ * Based on the information in the corresponding internet draft on LDAP password policy. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + * + * @see PasswordPolicyResponseControl + * @see Password Policy for LDAP + * Directories + */ +public class PasswordPolicyControl implements Control { + //~ Static fields/initializers ===================================================================================== + + /** OID of the Password Policy Control */ + public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1"; + + //~ Instance fields ================================================================================================ + + private boolean critical; + + //~ Constructors =================================================================================================== + + /** + * Creates a non-critical (request) control. + */ + public PasswordPolicyControl() { + this(Control.NONCRITICAL); + } + + /** + * Creates a (request) control. + * + * @param critical indicates whether the control is critical for the client + */ + public PasswordPolicyControl(boolean critical) { + this.critical = critical; + } + + //~ Methods ======================================================================================================== + + /** + * Retrieves the ASN.1 BER encoded value of the LDAP control. The request value for this control is always + * empty. + * + * @return always null + */ + public byte[] getEncodedValue() { + return null; + } + + /** + * Returns the OID of the Password Policy Control ("1.3.6.1.4.1.42.2.27.8.5.1"). + */ + public String getID() { + return OID; + } + + /** + * Returns whether the control is critical for the client. + */ + public boolean isCritical() { + return critical; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java new file mode 100644 index 0000000000..5eddfd2c31 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java @@ -0,0 +1,38 @@ +package org.springframework.security.ldap.ppolicy; + +import javax.naming.directory.DirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.LdapContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Obtains the PasswordPolicyControl from a context for use by other classes. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class PasswordPolicyControlExtractor { + private static final Log logger = LogFactory.getLog(PasswordPolicyControlExtractor.class); + + public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) { + LdapContext ctx = (LdapContext) dirCtx; + Control[] ctrls = null; + try { + ctrls = ctx.getResponseControls(); + } catch (javax.naming.NamingException e) { + logger.error("Failed to obtain response controls", e); + } + + for (int i = 0; ctrls != null && i < ctrls.length; i++) { + if (ctrls[i] instanceof PasswordPolicyResponseControl) { + return (PasswordPolicyResponseControl) ctrls[i]; + } + } + + return null; + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java new file mode 100755 index 0000000000..2435a68673 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlFactory.java @@ -0,0 +1,47 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.ppolicy; + +import javax.naming.ldap.Control; +import javax.naming.ldap.ControlFactory; + + +/** + * Transforms a control object to a PasswordPolicyResponseControl object, if appropriate. + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id$ + */ +public class PasswordPolicyControlFactory extends ControlFactory { + //~ Methods ======================================================================================================== + + /** + * Creates an instance of PasswordPolicyResponseControl if the passed control is a response control of this + * type. Attributes of the result are filled with the correct values (e.g. error code). + * + * @param ctl the control the check + * + * @return a response control of type PasswordPolicyResponseControl, or null + */ + public Control getControlInstance(Control ctl) { + if (ctl.getID().equals(PasswordPolicyControl.OID)) { + return new PasswordPolicyResponseControl(ctl.getEncodedValue()); + } + + return null; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java new file mode 100644 index 0000000000..887b65a970 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyData.java @@ -0,0 +1,12 @@ +package org.springframework.security.ldap.ppolicy; + +/** + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public interface PasswordPolicyData { + int getTimeBeforeExpiration(); + + int getGraceLoginsRemaining(); +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java new file mode 100644 index 0000000000..a727270d5e --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyErrorStatus.java @@ -0,0 +1,53 @@ +package org.springframework.security.ldap.ppolicy; + + +/** + * Defines status codes for use with PasswordPolicyException, with error codes (for message source lookup) and default + * messages. + * + *
+ * PasswordPolicyResponseValue ::= SEQUENCE {
+ * warning [0] CHOICE {
+ * timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ * graceAuthNsRemaining [1] INTEGER (0 .. maxInt)
+ * } OPTIONAL,
+ * error [1] ENUMERATED {
+ * passwordExpired (0), accountLocked (1),
+ * changeAfterReset (2), passwordModNotAllowed (3),
+ * mustSupplyOldPassword (4), insufficientPasswordQuality (5),
+ * passwordTooShort (6), passwordTooYoung (7),
+ * passwordInHistory (8)
+ * } OPTIONAL
+ * }
+ *
+ *
+ * @author Luke Taylor
+ * @since 3.0
+ */
+public enum PasswordPolicyErrorStatus {
+ PASSWORD_EXPIRED ("ppolicy.expired", "Your password has expired"),
+ ACCOUNT_LOCKED ("ppolicy.locked", "Account is locked"),
+ CHANGE_AFTER_RESET ("ppolicy.change.after.reset", "Your password must be changed after being reset"),
+ PASSWORD_MOD_NOT_ALLOWED ("ppolicy.mod.not.allowed", "Password cannot be changed"),
+ MUST_SUPPLY_OLD_PASSWORD ("ppolicy.must.supply.old.password", "The old password must be supplied"),
+ INSUFFICIENT_PASSWORD_QUALITY ("ppolicy.insufficient.password.quality", "The supplied password is of insufficient quality"),
+ PASSWORD_TOO_SHORT ("ppolicy.password.too.short", "The supplied password is too short"),
+ PASSWORD_TOO_YOUNG ("ppolicy.password.too.young", "Your password was changed too recently to be changed again"),
+ PASSWORD_IN_HISTORY ("ppolicy.password.in.history", "The supplied password has already been used");
+
+ private String errorCode;
+ private String defaultMessage;
+
+ private PasswordPolicyErrorStatus(String errorCode, String defaultMessage) {
+ this.errorCode = errorCode;
+ this.defaultMessage = defaultMessage;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public String getDefaultMessage() {
+ return defaultMessage;
+ }
+}
diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java
new file mode 100644
index 0000000000..3964232314
--- /dev/null
+++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyException.java
@@ -0,0 +1,22 @@
+package org.springframework.security.ldap.ppolicy;
+
+/**
+ * Generic exception raised by the ppolicy package.
+ * + * The status property should be checked for more detail on the cause of the exception. + * + * @author Luke Taylor + * @since 3.0 + */ +public class PasswordPolicyException extends RuntimeException { + private PasswordPolicyErrorStatus status; + + public PasswordPolicyException(PasswordPolicyErrorStatus status) { + super(status.getDefaultMessage()); + this.status = status; + } + + public PasswordPolicyErrorStatus getStatus() { + return status; + } +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java new file mode 100755 index 0000000000..4bb7de0aa8 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -0,0 +1,349 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.ppolicy; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import netscape.ldap.ber.stream.BERChoice; +import netscape.ldap.ber.stream.BERElement; +import netscape.ldap.ber.stream.BEREnumerated; +import netscape.ldap.ber.stream.BERInteger; +import netscape.ldap.ber.stream.BERIntegral; +import netscape.ldap.ber.stream.BERSequence; +import netscape.ldap.ber.stream.BERTag; +import netscape.ldap.ber.stream.BERTagDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataRetrievalFailureException; + + +/** + * Represents the response control received when a PasswordPolicyControl is used when binding to a + * directory. Currently tested with the OpenLDAP 2.3.19 implementation of the LDAP Password Policy Draft. It extends + * the request control with the control specific data. This is accomplished by the properties timeBeforeExpiration, + * graceLoginsRemaining. + *
+ * + * + * @author Stefan Zoerner + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControl.java,v 1.4 2009/03/04 07:25:07 itslxt Exp $ + * + * @see org.springframework.security.ldap.ppolicy.PasswordPolicyControl + * @see Stefan Zoerner's IBM developerworks + * article on LDAP controls. + */ +public class PasswordPolicyResponseControl extends PasswordPolicyControl { + //~ Static fields/initializers ===================================================================================== + + private static final Log logger = LogFactory.getLog(PasswordPolicyResponseControl.class); + + //~ Instance fields ================================================================================================ + + private byte[] encodedValue; + + private PasswordPolicyErrorStatus errorStatus; + + private int graceLoginsRemaining = Integer.MAX_VALUE; + private int timeBeforeExpiration = Integer.MAX_VALUE; + + //~ Constructors =================================================================================================== + + /** + * Decodes the Ber encoded control data. The ASN.1 value of the control data is:
+ * PasswordPolicyResponseValue ::= SEQUENCE { warning [0] CHOICE {
+ * timeBeforeExpiration [0] INTEGER (0 .. maxInt),
+ * graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL, error [1] ENUMERATED {
+ * passwordExpired (0), accountLocked (1),
+ * changeAfterReset (2), passwordModNotAllowed (3),
+ * mustSupplyOldPassword (4), insufficientPasswordQuality (5),
+ * passwordTooShort (6), passwordTooYoung (7),
+ * passwordInHistory (8) } OPTIONAL }
+ *
+ */
+ public PasswordPolicyResponseControl(byte[] encodedValue) {
+ this.encodedValue = encodedValue;
+
+ //PPolicyDecoder decoder = new JLdapDecoder();
+ PPolicyDecoder decoder = new NetscapeDecoder();
+
+ try {
+ decoder.decode();
+ } catch (IOException e) {
+ throw new DataRetrievalFailureException("Failed to parse control value", e);
+ }
+ }
+
+ //~ Methods ========================================================================================================
+
+ /**
+ * Returns the unchanged value of the response control. Returns the unchanged value of the response
+ * control as byte array.
+ */
+ public byte[] getEncodedValue() {
+ return encodedValue;
+ }
+
+ public PasswordPolicyErrorStatus getErrorStatus() {
+ return errorStatus;
+ }
+
+ /**
+ * Returns the graceLoginsRemaining.
+ *
+ * @return Returns the graceLoginsRemaining.
+ */
+ public int getGraceLoginsRemaining() {
+ return graceLoginsRemaining;
+ }
+
+ /**
+ * Returns the timeBeforeExpiration.
+ *
+ * @return Returns the time before expiration in seconds
+ */
+ public int getTimeBeforeExpiration() {
+ return timeBeforeExpiration;
+ }
+
+ /**
+ * Checks whether an error is present.
+ *
+ * @return true, if an error is present
+ */
+ public boolean hasError() {
+ return errorStatus != null;
+ }
+
+ /**
+ * Checks whether a warning is present.
+ *
+ * @return true, if a warning is present
+ */
+ public boolean hasWarning() {
+ return (graceLoginsRemaining != Integer.MAX_VALUE) || (timeBeforeExpiration != Integer.MAX_VALUE);
+ }
+
+ public boolean isExpired() {
+ return errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED;
+ }
+
+ public boolean isChangeAfterReset() {
+ return errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET;
+ }
+
+ public boolean isUsingGraceLogins() {
+ return graceLoginsRemaining < Integer.MAX_VALUE;
+ }
+
+ /**
+ * Determines whether an account locked error has been returned.
+ *
+ * @return true if the account is locked.
+ */
+ public boolean isLocked() {
+ return errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED;
+ }
+
+ /**
+ * Create a textual representation containing error and warning messages, if any are present.
+ *
+ * @return error and warning messages
+ */
+ public String toString() {
+ StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl");
+
+ if (hasError()) {
+ sb.append(", error: ").append(errorStatus.getDefaultMessage());
+ }
+
+ if (graceLoginsRemaining != Integer.MAX_VALUE) {
+ sb.append(", warning: ").append(graceLoginsRemaining).append(" grace logins remain");
+ }
+
+ if (timeBeforeExpiration != Integer.MAX_VALUE) {
+ sb.append(", warning: time before expiration is ").append(timeBeforeExpiration);
+ }
+
+ if (!hasError() && !hasWarning()) {
+ sb.append(" (no error, no warning)");
+ }
+
+ return sb.toString();
+ }
+
+ //~ Inner Interfaces ===============================================================================================
+
+ private interface PPolicyDecoder {
+ void decode() throws IOException;
+ }
+
+ //~ Inner Classes ==================================================================================================
+
+ /**
+ * Decoder based on Netscape ldapsdk library
+ */
+ private class NetscapeDecoder implements PPolicyDecoder {
+ public void decode() throws IOException {
+ int[] bread = {0};
+ BERSequence seq = (BERSequence) BERElement.getElement(new SpecificTagDecoder(),
+ new ByteArrayInputStream(encodedValue), bread);
+
+ int size = seq.size();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size + " elements");
+ }
+
+ for (int i = 0; i < seq.size(); i++) {
+ BERTag elt = (BERTag) seq.elementAt(i);
+
+ int tag = elt.getTag() & 0x1F;
+
+ if (tag == 0) {
+ BERChoice warning = (BERChoice) elt.getValue();
+
+ BERTag content = (BERTag) warning.getValue();
+ int value = ((BERInteger) content.getValue()).getValue();
+
+ if ((content.getTag() & 0x1F) == 0) {
+ timeBeforeExpiration = value;
+ } else {
+ graceLoginsRemaining = value;
+ }
+ } else if (tag == 1) {
+ BERIntegral error = (BERIntegral) elt.getValue();
+ errorStatus = PasswordPolicyErrorStatus.values()[error.getValue()];
+ }
+ }
+ }
+
+ class SpecificTagDecoder extends BERTagDecoder {
+ /** Allows us to remember which of the two options we're decoding */
+ private Boolean inChoice = null;
+
+ public BERElement getElement(BERTagDecoder decoder, int tag, InputStream stream, int[] bytesRead,
+ boolean[] implicit) throws IOException {
+ tag &= 0x1F;
+ implicit[0] = false;
+
+ if (tag == 0) {
+ // Either the choice or the time before expiry within it
+ if (inChoice == null) {
+ setInChoice(true);
+
+ // Read the choice length from the stream (ignored)
+ BERElement.readLengthOctets(stream, bytesRead);
+
+ int[] componentLength = new int[1];
+ BERElement choice = new BERChoice(decoder, stream, componentLength);
+ bytesRead[0] += componentLength[0];
+
+ // inChoice = null;
+ return choice;
+ } else {
+ // Must be time before expiry
+ return new BERInteger(stream, bytesRead);
+ }
+ } else if (tag == 1) {
+ // Either the graceLogins or the error enumeration.
+ if (inChoice == null) {
+ // The enumeration
+ setInChoice(false);
+
+ return new BEREnumerated(stream, bytesRead);
+ } else {
+ if (inChoice.booleanValue()) {
+ // graceLogins
+ return new BERInteger(stream, bytesRead);
+ }
+ }
+ }
+
+ throw new DataRetrievalFailureException("Unexpected tag " + tag);
+ }
+
+ private void setInChoice(boolean inChoice) {
+ this.inChoice = new Boolean(inChoice);
+ }
+ }
+ }
+
+/** Decoder based on the OpenLDAP/Novell JLDAP library */
+
+// private class JLdapDecoder implements PPolicyDecoder {
+//
+// public void decode() throws IOException {
+//
+// LBERDecoder decoder = new LBERDecoder();
+//
+// ASN1Sequence seq = (ASN1Sequence)decoder.decode(encodedValue);
+//
+// if(seq == null) {
+//
+// }
+//
+// int size = seq.size();
+//
+// if(logger.isDebugEnabled()) {
+// logger.debug("PasswordPolicyResponse, ASN.1 sequence has " +
+// size + " elements");
+// }
+//
+// for(int i=0; i < size; i++) {
+//
+// ASN1Tagged taggedObject = (ASN1Tagged)seq.get(i);
+//
+// int tag = taggedObject.getIdentifier().getTag();
+//
+// ASN1OctetString value = (ASN1OctetString)taggedObject.taggedValue();
+// byte[] content = value.byteValue();
+//
+// if(tag == 0) {
+// parseWarning(content, decoder);
+//
+// } else if(tag == 1) {
+// // Error: set the code to the value
+// errorCode = content[0];
+// }
+// }
+// }
+//
+// private void parseWarning(byte[] content, LBERDecoder decoder) {
+// // It's the warning (choice). Parse the number and set either the
+// // expiry time or number of logins remaining.
+// ASN1Tagged taggedObject = (ASN1Tagged)decoder.decode(content);
+// int contentTag = taggedObject.getIdentifier().getTag();
+// content = ((ASN1OctetString)taggedObject.taggedValue()).byteValue();
+// int number;
+//
+// try {
+// number = ((Long)decoder.decodeNumeric(new ByteArrayInputStream(content), content.length)).intValue();
+// } catch(IOException e) {
+// throw new LdapDataAccessException("Failed to parse number ", e);
+// }
+//
+// if(contentTag == 0) {
+// timeBeforeExpiration = number;
+// } else if (contentTag == 1) {
+// graceLoginsRemaining = number;
+// }
+// }
+// }
+}
diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java
index 806b7a031e..923ebc7a02 100644
--- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java
+++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java
@@ -23,6 +23,7 @@ import javax.naming.Name;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.ldap.ppolicy.PasswordPolicyData;
import org.springframework.util.Assert;
@@ -40,7 +41,7 @@ import org.springframework.util.Assert;
* @author Luke Taylor
* @version $Id$
*/
-public class LdapUserDetailsImpl implements LdapUserDetails {
+public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData {
//~ Instance fields ================================================================================================
@@ -52,6 +53,9 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
+ // PPolicy data
+ private int timeBeforeExpiration = Integer.MAX_VALUE;
+ private int graceLoginsRemaining = Integer.MAX_VALUE;
//~ Constructors ===================================================================================================
@@ -91,6 +95,14 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
return enabled;
}
+ public int getTimeBeforeExpiration() {
+ return timeBeforeExpiration;
+ }
+
+ public int getGraceLoginsRemaining() {
+ return graceLoginsRemaining;
+ }
+
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString()).append(": ");
@@ -217,5 +229,13 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
public void setUsername(String username) {
instance.username = username;
}
+
+ public void setTimeBeforeExpiration(int timeBeforeExpiration) {
+ instance.timeBeforeExpiration = timeBeforeExpiration;
+ }
+
+ public void setGraceLoginsRemaining(int graceLoginsRemaining) {
+ instance.graceLoginsRemaining = graceLoginsRemaining;
+ }
}
}
diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java
index 36b8cf7b4e..932e6f0921 100644
--- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java
+++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java
@@ -17,16 +17,16 @@ package org.springframework.security.ldap.userdetails;
import java.util.List;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.UserDetails;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
+import org.springframework.security.ldap.ppolicy.PasswordPolicyControl;
+import org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl;
import org.springframework.util.Assert;
-import org.springframework.ldap.core.DirContextAdapter;
-import org.springframework.ldap.core.DirContextOperations;
/**
@@ -86,6 +86,15 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
essence.addAuthority(authorities.get(i));
}
+ // Check for PPolicy data
+
+ PasswordPolicyResponseControl ppolicy = (PasswordPolicyResponseControl) ctx.getObjectAttribute(PasswordPolicyControl.OID);
+
+ if (ppolicy != null) {
+ essence.setTimeBeforeExpiration(ppolicy.getTimeBeforeExpiration());
+ essence.setGraceLoginsRemaining(ppolicy.getGraceLoginsRemaining());
+ }
+
return essence.createUserDetails();
}
diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java
new file mode 100644
index 0000000000..2b78c21e1e
--- /dev/null
+++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestSuite.java
@@ -0,0 +1,67 @@
+package org.springframework.security.ldap.ppolicy;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.ldap.authentication.BindAuthenticator;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
+
+/**
+ * Test cases which run against an OpenLDAP server.
+ * + * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java new file mode 100644 index 0000000000..2b78c21e1e --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/OpenLDAPIntegrationTestsuite.java @@ -0,0 +1,67 @@ +package org.springframework.security.ldap.ppolicy; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * Test cases which run against an OpenLDAP server. + *
+ * Run the script in the module root to start the server and import the data before running. + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +public class OpenLDAPIntegrationTestSuite { + PasswordPolicyAwareContextSource cs; + + @Before + public void createContextSource() throws Exception { + cs = new PasswordPolicyAwareContextSource("ldap://localhost:22389/dc=springsource,dc=com"); + cs.setUserDn("cn=admin,dc=springsource,dc=com"); + cs.setPassword("password"); + cs.afterPropertiesSet(); + } + + @Test + public void simpleBindSucceeds() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.authenticate(new UsernamePasswordAuthenticationToken("luke","password")); + } + + @Test(expected=LockedException.class) + public void repeatedBindWithWrongPasswordLocksAccount() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + for (int count=1; count < 4; count++) { + try { + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("lockme","wrong")); + LdapUserDetailsImpl ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } catch (BadCredentialsException expected) { + } + } + } + + @Test + public void passwordExpiryTimeIsDetectedCorrectly() throws Exception { + BindAuthenticator authenticator = new BindAuthenticator(cs); + authenticator.setUserDnPatterns(new String[] {"uid={0},ou=users"}); + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + Authentication a = provider.authenticate(new UsernamePasswordAuthenticationToken("expireme","password")); + PasswordPolicyData ud = (LdapUserDetailsImpl) a.getPrincipal(); + assertTrue(ud.getTimeBeforeExpiration() < Integer.MAX_VALUE && ud.getTimeBeforeExpiration() > 0); + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java new file mode 100644 index 0000000000..bcb27c0362 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControlTests.java @@ -0,0 +1,124 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.ppolicy; + +import junit.framework.TestCase; + +/** + * Tests for PasswordPolicyResponse. + * + * @author Luke Taylor + * @version $Id: PasswordPolicyResponseControlTests.java 2217 2007-10-27 00:45:30Z luke_t $ + */ +public class PasswordPolicyResponseControlTests extends TestCase { + //~ Methods ======================================================================================================== + + /** + * Useful method for obtaining data from a server for use in tests + */ +// public void testAgainstServer() throws Exception { +// Hashtable env = new Hashtable(); +// env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); +// env.put(Context.PROVIDER_URL, "ldap://gorille:389/"); +// env.put(Context.SECURITY_AUTHENTICATION, "simple"); +// env.put(Context.SECURITY_PRINCIPAL, "cn=manager,dc=security,dc=org"); +// env.put(Context.SECURITY_CREDENTIALS, "security"); +// env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); +// +// InitialLdapContext ctx = new InitialLdapContext(env, null); +// +// Control[] rctls = { new PasswordPolicyControl(false) }; +// +// ctx.setRequestControls(rctls); +// +// try { +// ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, "uid=bob,ou=people,dc=security,dc=org" ); +// ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "bobspassword"); +// Object o = ctx.lookup(""); +// +// System.out.println(o); +// +// } catch(NamingException ne) { +// // Ok. +// System.err.println(ne); +// } +// +// PasswordPolicyResponseControl ctrl = getPPolicyResponseCtl(ctx); +// System.out.println(ctrl); +// +// assertNotNull(ctrl); +// +// //com.sun.jndi.ldap.LdapPoolManager.showStats(System.out); +// } + + +// private PasswordPolicyResponseControl getPPolicyResponseCtl(InitialLdapContext ctx) throws NamingException { +// Control[] ctrls = ctx.getResponseControls(); +// +// for (int i = 0; ctrls != null && i < ctrls.length; i++) { +// if (ctrls[i] instanceof PasswordPolicyResponseControl) { +// return (PasswordPolicyResponseControl) ctrls[i]; +// } +// } +// +// return null; +// } + + public void testOpenLDAP33SecondsTillPasswordExpiryCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA0, 0x1, 0x21}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(33, ctrl.getTimeBeforeExpiration()); + } + + public void testOpenLDAP496GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x06, (byte) 0xA0, 0x04, (byte) 0xA1, 0x02, 0x01, (byte) 0xF0}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(496, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAP5GraceLoginsRemainingCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x05, (byte) 0xA0, 0x03, (byte) 0xA1, 0x01, 0x05}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasWarning()); + assertEquals(5, ctrl.getGraceLoginsRemaining()); + } + + public void testOpenLDAPAccountLockedCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x01}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isLocked()); + assertFalse(ctrl.hasWarning()); + } + + public void testOpenLDAPPasswordExpiredCtrlIsParsedCorrectly() { + byte[] ctrlBytes = {0x30, 0x03, (byte) 0xA1, 0x01, 0x00}; + + PasswordPolicyResponseControl ctrl = new PasswordPolicyResponseControl(ctrlBytes); + + assertTrue(ctrl.hasError() && ctrl.isExpired()); + assertFalse(ctrl.hasWarning()); + } +} diff --git a/ldap/template.mf b/ldap/template.mf index 64a2d80fe6..7bca39354e 100644 --- a/ldap/template.mf +++ b/ldap/template.mf @@ -19,5 +19,5 @@ Import-Template: org.springframework.core.io.*;version="[3.0.0, 3.1.0)", org.springframework.dao.*;version="[3.0.0, 3.1.0)";resolution:=optional, org.springframework.util.*;version="[3.0.0, 3.1.0)", - javax.naming.*;version="0";resolution:=optional - \ No newline at end of file + javax.naming.*;version="0";resolution:=optional, + netscape.ldap.ber.stream;version="[4.1, 5.0)";resolution:=optional \ No newline at end of file