Tuesday, March 27, 2007

Account Lockout Realm in Tomcat

(Quick links: SourceForge, CVS)

I am describing here a way to implement Account Lockout. We want to lock out those users who within a short period of time made multiple authentication attempts and failed. The Account Lockout feature is commonly used in Tomcat hardening and requested in security audit.

I derive my AccountLockoutDatasourceRealm from Tomcat's DataSourceRealm to include account lockout logic and return the reason for authentication failure. Finally, I modify login failure JSP to show a message specifying if the account is locked or password is merely incorrect.

To keep track of the password failures, I add two columns to the User table: LoginFailures number and LastLoginFailure date/time.

alter table User add (
LoginFailures number default 0,
LastLoginFailure date
);

comment on column User.LoginFailures is
'Number of consecutive login failures for the purposes of implementing Account Lockout';
comment on column User.LastLoginFailure is
'Date/time of the last login failure for the purposes of implementing Account Lockout';
The new AccountLockoutDatasourceRealm provides getters and setters for properties configuring account lockout, overrides authenticate() method using logic described below, and implements setExtendedStatus method of ExtendedStatusSetter interface.

New Realm Properties

failedAttemptsBeforeLockout
The maximum number of login failure attempts before the accounts is locked out or zero to disable lockout
lockoutDuration
The duration of lockout in minutes or zero for permanent lockout
getLoginStatsForUserStatement
SQL statement with one JDBC parameter (?) returning three values: the number of login failures, the date/time of the last login failure, and the current database date/time for the user whose id is specified by the parameter. For example,
 SELECT LoginFailures, LastLoginFailure, SYSDATE FROM User WHERE Id = ? 
resetAccountLockoutForUserStatement
SQL update statement with one JDBC parameter (?) resetting the number of login failures for the user whose id is specified by the parameter. For example,
 UPDATE User SET LoginFailures = 0 WHERE Id = ? 
recordFailureForUserStatement
SQL update statement with one JDBC parameter (?) incrementing the number of login failures and updating the date/time of the last login for the user whose id is specified by the parameter. For example,
 UPDATE User SET LoginFailures = NVL(LoginFailures, 0) + 1, LastLoginFailure = SYSDATE where Id = ? 

Account Lockout Logic

  1. Lockout feature is enabled if failedAttemptsBeforeLockout is greater than 0.
  2. If lockout is enabled, read login stats (LoginFailures, LastLoginFailure, and DatabaseDate) from the database.
  3. Lockout is considered expired, when
    1. lockout feature is not enabled, or
    2. lockout is not permanent and LastLoginFailure is more than lockoutDuration minutes before DatabaseDate.
  4. Account is considered locked, when the lockout is enabled and the LoginFailures is equal to or greater then failedAttemptsBeforeLockout.
  5. If lockout is enabled, account IS NOT locked, and lockout has expired, reset the lockout and reread the login stats.
  6. If lockout is enabled, account IS locked, lockout has expired, and the lockout is not permanent, reset the lockout and reread the login stats.
  7. If lockout is enabled and account IS locked, fail the login without checking the password and return.
  8. Otherwise (if lockout is not enabled or account is not locked), user check the password against the database.
  9. If password is not correct, increment LoginFailures and set LastLoginFailure to DatabaseDate in the database.
  10. Reread login stats. If account lockout enabled and account is locked according to the rules above, fail the login due to account lockout and return.
  11. If password is correct, reset the lockout and return login success.

Returning Login Failure Reason


We use code described in the post returning reason from a Tomcat Realm. In setExtendedStatus method we check if account is locked and if it is, we pass the message using a request attribute as follows.
if (isLockoutEnabled() && isAccountLocked(failureStats))
{
setMessage(request, "Account locked");
}

protected void setMessage(HttpServletRequest request, String message)
{
containerLog.debug("Setting extended status message to " + message);
request.setAttribute(ExtendedStatusSetter.LOGIN_FAILURE_MESSAGE_ATTR, message);
}

Displaying Login Failure Reason


Tomcat form authentication configures an error JSP, which is displayed when login fails. To display extended login failure reason, we check for the existence of our request attribute with the following code:
   <p class="error">Access Denied:
<c:choose>
<c:when test="${!(empty requestScope['com.ofc.tomcat.LOGIN_FAILURE_MESSAGE'])}">
<c:out value="${requestScope['com.ofc.tomcat.LOGIN_FAILURE_MESSAGE']}"/>
</c:when>
<c:otherwise>Invalid username and/or password</c:otherwise>
</c:choose>
</p>

To build the code, compile AccountLockoutDatasourceRealm below and ExtendedStatusFormAuthenticator and ExtendedStatusSetter from dzone snippet. Package the code and the mbeans-descriptor.xml into a jar, place the jar into Tomcat's server/lib, and restart tomcat.

Configuration


To configure your application, add the following lines to your context.xml
  <!-- Override Pragma:no-cache to work around the IE bug when app is served via SSL.
See http://www.mail-archive.com/tomcat-user%40jakarta.apache.org/msg151294.html
-->
<Valve className="com.ofc.tomcat.ExtendedStatusFormAuthenticator"
disableProxyCaching="false" />
<Realm className="com.ofc.tomcat.AccountLockoutDatasourceRealm"
dataSourceName="realm.datasource"
userTable="User"
userRoleTable="UserRole"
userNameCol="ID"
userCredCol="PASSWORD"
roleNameCol="ROLEID"
failedAttemptsBeforeLockout="3"
lockoutDuration="30"
getLoginStatsForUserStatement=
"SELECT LoginFailures, LastLoginFailure, SYSDATE FROM User where Id = ?"
resetAccountLockoutForUserStatement=
"UPDATE User SET LoginFailures = 0 WHERE Id = ?"
recordFailureForUserStatement=
"UPDATE User SET LoginFailures = NVL(LoginFailures, 0) + 1, LastLoginFailure = SYSDATE where Id = ?"
/>

Documentation and Source


I published source code in sourceforge lockout-realm project. Instructions are forthcoming.

Credits


This work was sponsored by Open Finance - a leading enabler of data consolidation for the financial services industry and Wealth Information Exchange - A Consolidated View of Wealth

2 comments:

Mike said...

Thanks for the useful extension!

Can you provide a sample logging.properties file?

I'm having a heck of a time trying to set the logging level for the container.

Frugal Guy said...

One issue I see is that this enables account enumeration attacks since it behaves differently with a bad userid versus a good userid. If I hit the site 10 times with a userid and a test password, I can tell if the userid is a good one or not.

Of course, it also allows DOS attacks once I've got a good userid, but all lockouts do that.