/images/resources/howtos/authentication/user-account-lockout.jpg

Manage User Account Lockout

On this page

The Curity Identity Server provides a username/password (HTML Form) authenticator out-of-the-box that can lock a user based on a configured number of failed authentication attempts. This can be coupled with the rolling-session-period setting to keep the user locked for a configured number of minutes. The lockout is session-based, and the number of failed login attempts is stored in a user session.

Sometimes, however, it's desirable to control the lockout centrally to not rely on the user session itself; and with that, lock the user across any session. This article outlines a way to configure a centralized lockout of a user with event listeners. This can be accomplished using either a bucket or the default accounts table, and a custom SQL query used by the credential manager in use.

Deprecated Feature

This tutorial relies on custom credential queries which were deprecated in version 10.0 of the Curity Identity Server. Use credential policies and Temporary Lockout instead.

Overview

The solution in this tutorial leverages two event listeners, one for FailedVerificationCredentialManagerEvent and one for SuccessfulVerificationCredentialManagerEvent.

When the FailedVerificationCredentialManagerEvent event triggers, the corresponding event listener increments a counter tied to the user and persists the value in a data store. When the counter reaches a configurable threshold (maxLogin), the event listener stores a timestamp value of current time + a configurable lockout time alongside the counter. The timestamp is in UNIX time format and represents the time the user is locked until. For every subsequent failed login attempt, the event listener updates the timestamp.

If the user credentials are correct and the stored lockout timer has passed, the SuccessfulVerificationCredentialManagerEvent triggers. In this case the corresponding event listener sets the timestamp to 0 and the failed login counter to 0, effectively resetting the lockout.

The credential manager uses a JDBC data source that operates with the credentials mode credentials-in-accounts-table-mode and a custom credential query. The query will compare the timestamp and current time, and will not produce a result if the account is locked. When no result is found, the authentication fails.

Buckets or Accounts

Depending on the Curity Identity Server deployment, there are different options to store the lockout counter and timestamp.

One option is to store the lockout counter and timestamp in the attributes column of the accounts table that holds account and password information. This will allow for a more optimized credential query since you do not need an SQL JOIN statements.

Alternatively, it is possible to store the lockout counter and timestamp in the buckets table instead. However, this forces the credential query to perform an SQL JOIN operation, which could negatively impact performance.

  1. Configure a new event listener, SystemEvent ListenersNew Event Listener, and name it account-lockout. Select the Script Event Listener as the type.
  2. Enable the Account Manager and choose the Account Manager used by the authenticator.
  3. Next to Procedure, click New. Give the Procedure a name, account-lockout-procedure, and click Create.
  4. In the JavaScript editor that pops up, replace all the code with the following:
javascript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
var listeners = {
'FailedVerificationCredentialManagerEvent': function (context, event) {
var maxLogin = 3; //Number of attempts before user is locked out
var lockoutTime = 60; //In seconds
var incrementedCounter;
var subject = event.getSubject();
logger.debug("Failed login by user {}.", subject);
var am = context.getAccountManager();
var account = am.getByUserName(subject);
logger.debug("Attributes retrieved from accounts for user {}: {}", subject, account);
//No value found for failedAuthenticationAttempts
if (account.failedAuthenticationAttempts === null)
{
incrementedCounter = 1;
}
else
{
incrementedCounter = parseInt(account.failedAuthenticationAttempts) + 1;
}
if(incrementedCounter < maxLogin)
{
account.failedAuthenticationAttempts = incrementedCounter;
account.lockoutUntilTime = 0;
am.updateAccount(account);
}
else
{
var currentTime = Math.round(new Date().getTime() / 1000);
var lockoutUntilTime = (currentTime + lockoutTime); //Add how long the user should be locked out to the current time
account.failedAuthenticationAttempts = incrementedCounter;
account.lockoutUntilTime = lockoutUntilTime.toString();
am.updateAccount(account);
}
},
'SuccessfulVerificationCredentialManagerEvent': function (context, event) {
var subject = event.getSubject();
logger.debug("Successful login by user {}. Resetting login counter in accounts table.", subject);
var am = context.getAccountManager();
var account = am.getByUserName(subject);
account.failedAuthenticationAttempts = 0;
account.lockoutUntilTime = 0;
am.updateAccount(account);
}
};

Configure parameters

Ensure to configure the maxLogin and lockoutTime variables in the script to match the requirements.

Credential Query

Make sure to read the timestamp from the account table when verifying credentials. For that, update the credential query on the data source of the credential manager that the authenticator is using. Go to FacilitiesData Sourcesdata source usedCredentials. Make sure to enable credentials-in-accounts-table-mode. In Credential Query enter the query.

sql
1234567891011121314
IF (SELECT JSON_VALUE(attributes, '$.lockoutUntilTime') FROM accounts WHERE accounts.username = :subjectId) IS NULL
SELECT account_id,
username AS userName,
password
FROM accounts WHERE username=:subjectId;
ELSE
SELECT
account_id,
username AS userName,
password
FROM accounts WHERE
username=:subjectId AND
( (SELECT JSON_VALUE(attributes, '$.lockoutUntilTime')) <
( SELECT DATEDIFF_BIG(second, '1970-01-01 00:00:00', GETUTCDATE())) );

MS SQL specific

The SQL query used in both solutions is specific to MS SQL and is tested with version 2017-CU24-ubuntu-16.04. But, it should work with other versions of MS SQL as well.

It would also be possible to achieve the same approach using different databases, as long as they enable similar functions to read the JSON blob and can handle IF/ELSE statements in the query.

Optimization

The complexity of the SQL query could have an impact on performance due to using the JSON_VALUE function. To improve performance related to the JSON_VALUE function, read this post: Query performance for JSON objects inside SQL Server using JSON_VALUE function - Blog IT.

Newsletter

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Newsletter

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial