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

Friday, March 23, 2007

Shell script to locate a Java class

The following shell script will locate a Java class in CLASSPATH and additional jars, zip files, or directories specified on the command line. For example,

$ CLASSPATH= findClass DataSourceRealm /usr/local/tomcat/server/lib/*.jar
---- Searching for DataSourceRealm
---- Searching in /usr/local/tomcat/server/lib/catalina-ant.jar
---- Searching in /usr/local/tomcat/server/lib/catalina.jar
org/apache/catalina/realm/DataSourceRealm.class
---- Searching in /usr/local/tomcat/server/lib/commons-beanutils.jar
...
**** DataSourceRealm found in /usr/local/tomcat/server/lib/catalina.jar

Source

#!/bin/bash
#****h* bin/findClass
# NAME
# findClass - searches for a Java class in directories, jars, zip files, and CLASSPATH
# ARGUMENTS
# * 1 - Java class name or substring
# * ... - additional directories, jars, zip files
# RETURN VALUE
# * exit code 0, if the class is found
# * exit code 1, if the class is not found
# SOURCE
if [ $# -eq 0 ]; then
echo "Usage: findClass <CLASSNAME> [ FILE ... ]"
exit
fi
class=`echo $1| tr . /`
shift
allDirs="$(echo "$@" $CLASSPATH| perl -F: -ane 'print join(" ",@F);')"
printf -- "---- Searching for %s\n" "$class" >&2
foundIn=()
for dir in $allDirs; do
printf -- "---- Searching in %s\n" "$dir" >&2
case $dir in
*.zip)
unzip -v "$dir" | grep $class && foundIn[${#foundIn}]="$dir"
;;
*.jar)
jar -tf "$dir" | grep $class && foundIn[${#foundIn}]="$dir"
;;
*)
find "$dir" -print | grep $class && foundIn[${#foundIn}]="$dir"
;;
esac
done
if [ ${#foundIn} -eq 0 ]; then
printf -- "**** %s not found\n" "$class" >&2
exit 1
else
printf -- "**** %s found in %s\n" "$class" "${foundIn[*]}" >&2
fi
#****

Thursday, March 22, 2007

I am glad I am not using Windows

Just read a scary and fascinating story about Gozi Windows trojan, which sees through SSL and steals bank passwords, security questions, and the newfangled security pictures. I am so glad my desktop machine runs Linux, in which malware will not be able to install itself as a layer between the browser and the SSL code. More details.

Returning login failure reason in a Tomcat Realm

When Tomcat Realm authenticates a user via its Realm, there is no way for the Realm to tell the application the exact reason of a failure. The realm either succeeds and returns a Principal object or fails and returns null. So, when I needed to implement account lockout after several unsuccessful logins, I couldn't show the user the difference between a wrong password and account being locked out.

Digging deeper into Tomcat code, I found that the Realm is being invoked from FormAuthenticator Valve. It is possible to extend the Valve so that in case of authentication failure, the Valve adds failure reason as an HttpServletRequest attribute. More details in ExtendedStatusFormAuthenticator code.