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 (The new AccountLockoutDatasourceRealm provides getters and setters for properties configuring account lockout, overrides
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';
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
- Lockout feature is enabled if
failedAttemptsBeforeLockout
is greater than 0. - If lockout is enabled, read login stats (LoginFailures, LastLoginFailure, and DatabaseDate) from the database.
- Lockout is considered expired, when
- lockout feature is not enabled, or
- lockout is not permanent and LastLoginFailure is more than
lockoutDuration
minutes before DatabaseDate.
- Account is considered locked, when the lockout is enabled and the LoginFailures is equal to or greater then
failedAttemptsBeforeLockout
. - If lockout is enabled, account IS NOT locked, and lockout has expired, reset the lockout and reread the login stats.
- If lockout is enabled, account IS locked, lockout has expired, and the lockout is not permanent, reset the lockout and reread the login stats.
- If lockout is enabled and account IS locked, fail the login without checking the password and return.
- Otherwise (if lockout is not enabled or account is not locked), user check the password against the database.
- If password is not correct, increment LoginFailures and set LastLoginFailure to DatabaseDate in the database.
- 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.
- 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