Many institutions are using Shibboleth for unified single sign-on between both internal and external web application. Shibboleth is an authentication engine and, as its backend, it can use a variety of sources for authentication including LDAP, a SQL database or other resources. It simply deals with authentication, so more advanced configurations, such as systems which allow grace logins after a password expires, may require more customization. The following tutorial shows how to use Shibboleth with a Novell e-Directory server that allows grace logins after a user’s password has expired.

Novell e-Directory server allows for logins to occur after the password expiration date has passed. When this happens the LDAP bind operation occurs successfully, but a custom header with the error -233 is sent in the bind response. Standard LDAP requests from Java using JNDI cannot see this response header. To see this response in the bind, Novell specific Java API must be used.

Rather than change the JAAS layer to use Novell’s LDAP API, a usable approach is to read a user’s LDAP attributes after a login to verify the password hasn’t expired and, if so, to inform the user that he or she must change that password. To do this, extend the edu.vt.middleware.ldap.auth.handler.BindAuthenticationHandler handler as seen in the following example.

public class GraceAuthenticationHandler extends BindAuthenticationHandler {
  
  protected final Log logger = LogFactory.getLog(this.getClass());

  public static final String MALFORMED_EXPIRATION_DATE = "-90555";
  
  public static final String PASSWORD_EXPIRED = "-223";
  
  public GraceAuthenticationHandler() {
  }

  public GraceAuthenticationHandler(final AuthenticatorConfig ac) {
    this.setAuthenticatorConfig(ac);
  }

  public void authenticate(final ConnectionHandler ch, final AuthenticationCriteria ac) throws NamingException {

    Ldap ldap = null;
    
    try {
        super.authenticate(ch, ac);

        ldap = new Ldap(this.config);
        
        Attributes attrs = ldap.getAttributes(ac.getDn(),new String[] { "passwordExpirationTime", "loginGraceRemaining" } );        
        
        String exp = attrs.get("passwordExpirationTime") != null ? (String) attrs.get("passwordExpirationTime").get(0) : "";
        String graceLogins = attrs.get("loginGraceRemaining") != null ? (String) attrs.get("loginGraceRemaining").get(0) : "";

        logger.debug("UserDN: " + ac.getDn() );
        logger.debug("Expiration Date: " + exp);
        logger.debug("GraceLogins: " + graceLogins);
        
        Date date;
        try {
          date = new SimpleDateFormat("yyyyMMddHHmmss").parse(exp);
        }
        catch (ParseException p) {
          throw new AuthenticationException(String.format("Error (%s): User's password expiration time is in incorrect format",MALFORMED_EXPIRATION_DATE));
        }
        
        if(date.before(new Date())) {
          throw new AuthenticationException(String.format("Error (%s): Password expired on %s. %s grace logins remaining",PASSWORD_EXPIRED,exp,graceLogins));
        }

    } catch(RuntimeException e) {
      throw e;
    } finally {
      if (ldap != null) {
        ldap.close();
      }
    }
  }

  public GraceAuthenticationHandler newInstance()
  {
    return new GraceAuthenticationHandler(this.config);
  }
}

It’s important to note that the password expired error code we are setting is the same as the Novell error code: -223. For an invalid expiration date, a number has been chosen randomly outside the range of standard Novell error codes. Typically, this error will be seen if the user specified in the login.config does not have permission to view the passwordExpirationTime and loginGraceRemaining attributes.

This custom authentication handler must be added to the login.config as seen below.

ShibUserPassAuth {

  edu.vt.middleware.ldap.jaas.LdapLoginModule required
      host="ldaps://ldap.example.edu"
      base="o=example"
      bindDn="cn=IDMUser,ou=admins,o=example"
      bindCredential="somePassword"
      ssl="true"
      userField="uid"
      subtreeSearch="true"
      authenticationHandler="edu.example.shibboleth.idm.auth.GraceAuthenticationHandler";
};

When the JAAS layer encounters this exception, it copies the message from the exception into a new LoginException. Because of this, exceptions with new custom attributes cannot be used. Instead, the error string must be parsed to determine the cause of the error. This can be done in the login.jsp found in the idp.war by adding the following:

<%@ page import="edu.example.shibboleth.idm.auth.GraceAuthenticationHandler" %>

...

<% if (request.getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY) != null) {

  Exception exception = (Exception) request.getAttribute(LoginHandler.AUTHENTICATION_EXCEPTION_KEY);
  String loginMsg = exception.getMessage().trim();
  String niceMsg = "";

  String changePassUrl = "https://example.edu/changePasswordApp";

  //display error message
  if(loginMsg.contains("Cannot authenticate dn, invalid dn") || loginMsg.contains("669")) {
  niceMsg = "Invalid username or password";
  }
  else if(loginMsg.contains(GraceAuthenticationHandler.MALFORMED_EXPIRATION_DATE)) {
  niceMsg = "Cannot authenticate. Account has invalid expiration date";
  }
  else if(loginMsg.contains(GraceAuthenticationHandler.PASSWORD_EXPIRED)) {
  niceMsg = String.format("Your password has expired. Click <a href=\"%s\">here</a> to change your password",changePassUrl);
  }
  else if(loginMsg.contains("222")) {
  niceMsg = "Password has expired and you are out of grace logins. Please call the helpdesk.";
  }
  else if(loginMsg.contains("220")) {
  niceMsg = "Account is disabled";
  }
  else if(loginMsg.contains("217")) {
  niceMsg = "Number of concurrent connections exceeded";
  }
  else if(loginMsg.contains("197")) {
  niceMsg = "Account is locked";
  }
  else if(loginMsg.contains("218")) {
  niceMsg = "Login time limited";
  }
  else {
  niceMsg = "An unknown authentication error occured";
  }

%>
<span style="color:red; background-color:white; padding: 5px;"><%= niceMsg %></span>
<% } %>

The above example handles the most common Novell e-Directory error codes. It is important to note that with this example, the user is forced to change his or her password even if grace logins remain. Allowing the user to continue to authenticate after grace logins have expired would require an additional custom login servlet and possible a custom login handler.

Special thanks goes to Daniel Fisher who provided most of the code for the authentication handler, as well as the other users on the Shibboleth-dev mailing list who helped me with writing and debugging.

1 LDAP errors returned when NDS login, password, time and address restrictions are set Novell. February 12, 2003

Comments

Vern DeHaven 2012-06-29

Great post, thanks for all the work! I wanted to post a small modification required to handle discrepancies in time zones between LDAP and Java. Our LDAP returns date/times in UTC, whereas the Date class returns the date/time in the current time zone when instantiated. SimpleDateFormat.parse() also assumes dates are in the current time zone. To force everything to UTC for a true comparison, one may use:

import java.text.ParseException;

import java.util.Calendar;

import java.util.TimeZone;

import java.text.SimpleDateFormat;

import java.util.Date;

import javax.naming.AuthenticationException;

import javax.naming.NamingException;

import javax.naming.directory.Attributes;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import edu.vt.middleware.ldap.Ldap;

import edu.vt.middleware.ldap.auth.AuthenticatorConfig;

import edu.vt.middleware.ldap.auth.handler.AuthenticationCriteria;

import edu.vt.middleware.ldap.auth.handler.BindAuthenticationHandler;

import edu.vt.middleware.ldap.handler.ConnectionHandler;

// Version 0.2: VFD 2012-06-25: Moved all dates to UTC to avoid time zone conversion issues on password expiration.

public class GraceAuthenticationHandler extends BindAuthenticationHandler {

protected final Log logger = LogFactory.getLog(this.getClass());

public static final String MALFORMED_EXPIRATION_DATE = "-10555";

public static final String PASSWORD_EXPIRED = "-10747";

public GraceAuthenticationHandler() {

}

public GraceAuthenticationHandler(final AuthenticatorConfig ac) {

this.setAuthenticatorConfig(ac);

}

public void authenticate(final ConnectionHandler ch, final AuthenticationCriteria ac) throws NamingException {

Ldap ldap = null;

try {

super.authenticate(ch, ac);

ldap = new Ldap(this.config);

Attributes attrs = ldap.getAttributes(ac.getDn(),new String[] { "passwordExpirationTime", "loginGraceRemaining" } );

String exp = attrs.get("passwordExpirationTime") != null ? (String) attrs.get("passwordExpirationTime").get(0) : "";

String graceLogins = attrs.get("loginGraceRemaining") != null ? (String) attrs.get("loginGraceRemaining").get(0) : "";

logger.debug("UserDN: " + ac.getDn() );

logger.debug("Expiration Date: " + exp);

logger.debug("GraceLogins: " + graceLogins);

Date dateLDAP;

try {

dateLDAP = new SimpleDateFormat("yyyyMMddHHmmss").parse(exp);

}

catch (ParseException p) {

throw new AuthenticationException(String.format("Error (%s): User's password expiration time is in incorrect format",MALFORMED_EXPIRATION_DATE));

}

//

// LDAP string parsed above is in the GMT time zone. The parsing above assumes it is in the current

// locale. Moving everything to GMT before comparison.

Calendar calLDAP = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));

calLDAP.setTime(dateLDAP);

//if(date.before(new Date())) {

if(calLDAP.before(Calendar.getInstance(TimeZone.getTimeZone("GMT+0")))) {

throw new AuthenticationException(String.format("Error (%s): Password expired on %s. %s grace logins remaining",PASSWORD_EXPIRED,exp,graceLogins));

}

//

} catch(RuntimeException e) {

throw e;

} finally {

if (ldap != null) {

ldap.close();

}

}

}

public GraceAuthenticationHandler newInstance()

{

return new GraceAuthenticationHandler(this.config);

}

}

Comments are closed. Why?