Saturday, March 31. 2012
LDAP password policies and JavaEE
If you remember some time ago a three chapter series about JavaEE certificate login were published in this blog. In all of them an LDAP server was used as the identity store (in those entries OpenLDAP was configured) and, needless to say, that is the common situation. This time the entry is going to extend that subject mixing JavaEE, LDAP and password policies. A password policy is a set of rules governing the use of passwords in the repository, it manages concepts like complexity (minimum length, numbers, capitals, history passwords, dictionary,...), expiration (maximum and minimum age, expiration by inactivity), locking (failed attempts lock the account for a specified time or forever) and so on. Obviously any JavaEE application which supports password policies needs to understand the state of the user password and acts accordingly. That is what this entry talks about.
First of all the general diagram of all the possible user states is commented. More or less all LDAP servers (and, in general, any user repository) manage the following ideas:
Expiration of the password. The password has a maximum age and, before that, it has to be changed. After the specified age the user password is expired and he cannot login to the system. Nevertheless repositories usually use some hints to avoid a complete expiration:
Warning period. A period before the deadline time in which the system warns the user his password is about to expire.
Grace logins. A number of extra logins given to the user for changing his password after expiration time.
Forcing password change. Very related to the previous point we found the forcing password change, for example this state can be configured after an administrative password reset or after user creation.
Account locking. Locking configuration for password policies is quite common and it means that the user account will be locked if a specified number of consecutive failed login attempts are performed (usually it is configured the number of attempts and the period of time the account remains locked).
Account desactivation. Finally (out of policy passwords) any account can be administratively disabled for some reason.
With all those cases the following account (password) states can be defined:
DISABLED: User is administratively disabled.
LOCKED: User is locked for some reason (failed attempts for example).
EXPIRED: User account is expired (password age, inactivity).
PASSWORD_MUST_CHANGE: The user can login but the password needs to be changed right now.
WARNING_EXPIRED: The user can login but the password is going to expire soon.
OK: Normal login.
So now the question is how to guess the state of the user account when a LDAP server is used. The ldap operation BIND is used to check the credentials of any user and it can return SUCCESS (code 0), INVALID_CREDENTIALS (49) or any other internal error (see LDAP return codes). What servers usually use to display account state at BIND are controls and/or error messages. Response controls are a kind of tags that can be attached to any LDAP response in order to remark something. A response control is defined by an OID (Object IDentifier) like any other object in the LDAP standard and has a value. An error message is a descriptive text that the LDAP server gives to the client in an error response. So any account/password state should be guessed using only controls after a bind success and controls/error messages after a bind error (obviously success responses do not have error messages).
Exemplifying the previous paragraph I am going to explain how Netscape based LDAP servers manage account states. When I say Netscape I mean any LDAP server that comes from Netscape, nowadays there are a lot of them, 389 Directory Server (I do not know if if it supports all the commented policy features), Oracle Unified Directory (OUD), Oracle Directory Server Enterprise Edition (ODSEE), OpenDS, OpenDJ,... With an old OpenDS that I have already installed in my laptop, I configured detailed error bind responses[1], then a new password policy was created[2] and, finally, the policy was assigned to my user[3]. The result of some testing was the following table, which summarizes how a BIND operation is responded in each account state (good password is always sent).
ERROR CODE | CONTROLS | ERROR MESSAGE | |
---|---|---|---|
DISABLED | 49 (invalid credentials) | Rejecting a bind request for user USER_DN because the account has been administrative disabled | |
LOCKED | 49 (invalid credentials) | Rejecting a bind request for user USER_DN because the account has been locked due to too many failed authentication attempts | |
EXPIRED | 49 (invalid credentials) | 2.16.840.1.113730.3.4.4=0 | Rejecting a bind request for user USER_DN because that user's password is expired |
PASSWORD MUST CHANGE | 0 (success) | 2.16.840.1.113730.3.4.4=0 | |
WARNING EXPIRED | 0 (success) | 2.16.840.1.113730.3.4.5=X (X=number of seconds to expire) |
|
OK | 0 (success) |
So it is quite easy to understand, Netscape LDAP servers manage two controls:
The Netscape password expiring control. This control has an OID of 2.16.840.1.113730.3.4.5, it is only returned when the account is in the warning period, and the value is a string representation of the length of time in seconds until the password actually expires.
The Netscape password expired control. This control has an OID of 2.16.840.1.113730.3.4.4 and means the account is expired, the value is always 0 (there is no time until expiration, it is already expired).
And as you see guessing the user state is just a question of checking those two control tags and the returned error messages.
Now a second important part, how Java lets manage error responses and controls. Error responses to a Bind are always in the message of the AuthenticationException. Any error 49 (INVALID_CREDENTIALS) throws that exception and its message is composed using the code and the error response, both are concatenated. Controls are managed by the so-named interface and they need an LdapContext (method getResponseControls) to be collected. A tricky reconnect has to be used in case response controls needed to be retrieved after a failed bind (the bind in Java is hidden inside the creation of the Context, if an error is returned, context is never created, so no method can be called). I present a little Bind.java which just does it, connects using a manager user (like a common Java application would do) and then performs a user bind recollecting controls and error. I used this program to complete the table presented before. The main code is displayed below.
Hashtableenv = new Hashtable (7); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, args[0]); // ldap url env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, args[1]); // manager dn env.put(Context.SECURITY_CREDENTIALS, args[2]); // manager password env.put(Context.SECURITY_PROTOCOL, "plain"); env.put("com.sun.jndi.ldap.connect.pool", "false"); LdapContext ctx = null; System.out.println("Using LDAP url " + args[0] + "..."); try { System.out.print("Connecting as manager " + args[1] + "... "); ctx = new InitialLdapContext(env, null); System.out.println("OK"); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, args[3]); // user dn ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, args[4]); // user password System.out.print("Connecting as user " + args[3] + "... "); ctx.reconnect(null); System.out.println("OK"); } catch (NamingException ex) { System.out.println("ERR: " + ex.getMessage()); } finally { if (ctx != null) { Control[] controls = ctx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { System.out.println(" Control: " + controls[i].getID() + " | crit: " + controls[i].isCritical() + " | val: '" + new String(controls[i].getEncodedValue()) + "'"); } } ctx.close(); } }
Finally I modified my CertSecurityCustom project (the one I used in custom certificate login entry) to retrieve the account state and show correct messages at login time. Besides if the user is in PASSWORD_MUST_CHANGE state he is forced to change the password, and if he is in WARNING_EXPIRED a message is shown in the index page. The video presents a user whose password is in the warning period (appropriate warning message is shown). Then using a common ldapmodify command the user password is reset, so he must change it immediately. After that the same user fails several times until locked message is shown.
I hope this entry would be good for demythifying LDAP password states. I personally knew the ideas behind this matter but I had never studied it very deeply. It was a nice time doing the little program and filling the table. I suppose that any other LDAP server would work in a very similar way and the ideas presented here could be easily extended to cover it.
First think! Then act!
[1] By default OpenDS does not give a detailed error response (security reasons). It should be configured like this:
$ ./dsconfig set-global-configuration-prop -h localhost -p 4444 --trustAll \ -D "cn=Directory Manager" -w **** -n --set return-bind-error-messages:true
[2] I created a sample password policy with some complexity requirements, very short age, locking for 30 minutes after 5 failed attempts and force password changing after admin reset:
$ ./dsconfig create-password-policy -h localhost -p 4444 -D "cn=Directory Manager" \ -w **** --trustAll -n --policy-name "Sample Password Policy" \ --set password-attribute:userPassword --set default-password-storage-scheme:"Salted SHA-1"\ --set lockout-duration:"30 minutes" --set lockout-failure-count:5 \ --set max-password-age:"2 hours" --set password-expiration-warning-interval:"1 hours" \ --set force-change-on-reset:true --set password-history-count:8 \ --set password-validator:"Length-Based Password Validator"
[3] My user was assigned to this policy, it is just done assigning a special attribute to the user:
$ ldapmodify -D "cn=Directory Manager" -w **** -h localhost -p 1389 dn: uid=ricky,ou=people,dc=example,dc=com changetype: modify add: ds-pwp-password-policy-dn ds-pwp-password-policy-dn: cn=Sample Password Policy,cn=Password Policies,cn=config
Comments