Skip to content

Commit

Permalink
CAS-1418 Add support for AD password expiration warnings.
Browse files Browse the repository at this point in the history
  • Loading branch information
serac committed Feb 5, 2014
1 parent aec01fd commit 7c19a3b
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public class LdapAuthenticationHandler implements AuthenticationHandler {
@NotNull
protected Map<String, String> principalAttributeMap = Collections.emptyMap();

/** List of additional attributes to be fetched but are not principal attributes. */
@NotNull
protected List<String> additionalAttributes = Collections.emptyList();

/** Set of LDAP attributes fetch from an entry as part of the authentication process. */
private String[] authenticatedEntryAttributes;

Expand Down Expand Up @@ -141,6 +145,18 @@ public void setPrincipalAttributeMap(final Map<String, String> attributeNameMap)
this.principalAttributeMap = attributeNameMap;
}

/**
* Sets the list of additional attributes to be fetched from the user entry during authentication.
* These attributes are <em>not</em> bound to the principal.
* <p>
* A common use case for these attributes is to support password policy machinery.
*
* @param additionalAttributes List of operational attributes to fetch when resolving an entry.
*/
public void setAdditionalAttributes(final List<String> additionalAttributes) {
this.additionalAttributes = additionalAttributes;
}

/**
* Sets the LDAP password policy configuration. If none is defined, password expiration policy support will be
* disabled.
Expand Down Expand Up @@ -262,6 +278,7 @@ public void initialize() {
attributes.add(this.principalIdAttribute);
}
attributes.addAll(this.principalAttributeMap.keySet());
attributes.addAll(this.additionalAttributes);
this.authenticatedEntryAttributes = attributes.toArray(new String[attributes.size()]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you 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 the following location:
*
* 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.jasig.cas.authentication.support;

import java.util.List;

import org.jasig.cas.Message;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.ldaptive.ConnectionFactory;
import org.ldaptive.LdapException;
import org.ldaptive.Response;
import org.ldaptive.SearchExecutor;
import org.ldaptive.SearchFilter;
import org.ldaptive.SearchResult;
import org.ldaptive.SearchScope;
import org.ldaptive.auth.AccountState;
import org.ldaptive.auth.AuthenticationResponse;

/**
* Provides platform-specific account state handling for Active Directory.
*
* @author Marvin S. Addison
*/
public class ActiveDirectoryAccountStateHandler extends DefaultAccountStateHander {

/** Number of milliseconds between standard Unix era, 1/1/1970, and filetime start, 1/1/1601. */
private static final long ERA_OFFSET = 11644473600000L;

/** Source of LDAP connections. */
private ConnectionFactory connectionFactory;

/** Root DN containing CN=Builtin entry. */
private String rootDN;

/** Maximum password age. */
private Duration maxPasswordAge;


/**
* Initializes this component. MUST be called prior to
* {@link #handle(AuthenticationResponse, LdapPasswordPolicyConfiguration)}.
*/
public void initialize() {
// Query for the default domain policy maxPwdAge attribute in the domain entry.
// NOTE: does not support Windows Server 2008 Fine-Grained Password Policies.
final SearchExecutor se = new SearchExecutor();
se.setBaseDn(rootDN);
se.setSearchFilter(new SearchFilter("(objectClass=*)"));
se.setReturnAttributes("maxPwdAge");
se.setSearchScope(SearchScope.OBJECT);
try {
final Response<SearchResult> response = se.search(connectionFactory);
maxPasswordAge = parseDeltaTime(response.getResult().getEntry().getAttribute("maxPwdAge").getStringValue());
} catch (final LdapException e) {
throw new IllegalStateException("LDAP error searching for maxPwdAge", e);
}
}

public void setConnectionFactory(final ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}

public void setRootDN(final String rootDN) {
this.rootDN = rootDN;
}

@Override
protected void handleWarning(
final AccountState.Warning warning,
final AuthenticationResponse response,
final LdapPasswordPolicyConfiguration configuration,
final List<Message> messages) {
super.handleWarning(warning, response, configuration, messages);
final Instant pwdLastSet = parseFileTime(response.getLdapEntry().getAttribute("pwdLastSet").getStringValue());
final Days ttl = Days.daysBetween(Instant.now(), pwdLastSet.plus(maxPasswordAge));
if (ttl.getDays() < configuration.getPasswordWarningNumberOfDays()) {
messages.add(new PasswordExpiringWarningMessage(
"Password expires in {0} days. Please change your password at <href=\"{1}\">{1}</a>",
ttl.getDays(),
configuration.getPasswordPolicyUrl()));
}
}

/**
* Parses a time value in delta time format, http://msdn.microsoft.com/en-us/library/cc232152.aspx.
*
* @param deltaTimeString Negative number of 100-nanosecond intervals.
*
* @return Corresponding Joda time duration.
*/
private static Duration parseDeltaTime(final String deltaTimeString) {
final long deltaTime = -Long.parseLong(deltaTimeString);
return new Duration(deltaTime / 10000L);
}

/**
* Parses a Microsoft FILETIME date, http://msdn.microsoft.com/en-us/library/windows/desktop/ms724290(v=vs.85).aspx.
*
* @param fileTimeString Number of 100-nanosecond intervals since Jan 1, 1601.
*
* @return Corresponding Joda time instant.
*/
private static Instant parseFileTime(final String fileTimeString) {
return new Instant(Long.parseLong(fileTimeString) / 10000L - ERA_OFFSET);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -75,31 +74,81 @@ public List<Message> handle(final AuthenticationResponse response, final LdapPas
throws LoginException {

final AccountState state = response.getAccountState();
if (state == null) {
return Collections.emptyList();
}
final LoginException error = ERROR_MAP.get(state.getError());
if (error != null) {
throw error;
final AccountState.Error error;
final AccountState.Warning warning;
if (state != null) {
error = state.getError();
warning = state.getWarning();
} else {
error = null;
warning = null;
}
final List<Message> messages = new ArrayList<Message>();
if (state.getWarning() != null) {
final Calendar expDate = state.getWarning().getExpiration();
final Days ttl = Days.daysBetween(Instant.now(), new Instant(expDate));
if (ttl.getDays() < configuration.getPasswordWarningNumberOfDays()) {
messages.add(new PasswordExpiringWarningMessage(
"Password expires in {0} days. Please change your password at <href=\"{1}\">{1}</a>",
ttl.getDays(),
configuration.getPasswordPolicyUrl()));
}
if (state.getWarning().getLoginsRemaining() > 0) {
messages.add(new Message(
"password.expiration.loginsRemaining",
"You have {0} logins remaining before you MUST change your password.",
state.getWarning().getLoginsRemaining()));
handleError(error, response, configuration, messages);
handleWarning(warning, response, configuration, messages);
return messages;
}

/**
* Handle an account state error produced by ldaptive account state machinery.
* <p>
* Override this method to provide custom error handling.
*
* @param error Account state error.
* @param response Ldaptive authentication response.
* @param configuration Password policy configuration.
* @param messages Container for messages produced by account state error handling.
*
* @throws LoginException On errors that should be communicated as login exceptions.
*/
protected void handleError(
final AccountState.Error error,
final AuthenticationResponse response,
final LdapPasswordPolicyConfiguration configuration,
final List<Message> messages)
throws LoginException {

final LoginException ex = ERROR_MAP.get(error);
if (ex != null) {
throw ex;
}
}


/**
* Handle an account state warning produced by ldaptive account state machinery.
* <p>
* Override this method to provide custom warning message handling.
*
* @param error Account state warning.
* @param response Ldaptive authentication response.
* @param configuration Password policy configuration.
* @param messages Container for messages produced by account state warning handling.
*/
protected void handleWarning(
final AccountState.Warning warning,
final AuthenticationResponse response,
final LdapPasswordPolicyConfiguration configuration,
final List<Message> messages) {

if (warning == null) {
return;
}

final Calendar expDate = warning.getExpiration();
final Days ttl = Days.daysBetween(Instant.now(), new Instant(expDate));
if (ttl.getDays() < configuration.getPasswordWarningNumberOfDays()) {
messages.add(new PasswordExpiringWarningMessage(
"Password expires in {0} days. Please change your password at <href=\"{1}\">{1}</a>",
ttl.getDays(),
configuration.getPasswordPolicyUrl()));
}
if (warning.getLoginsRemaining() > 0) {
messages.add(new Message(
"password.expiration.loginsRemaining",
"You have {0} logins remaining before you MUST change your password.",
warning.getLoginsRemaining()));

}
}
return messages;
}
}

0 comments on commit 7c19a3b

Please sign in to comment.