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

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.

Overview

The solution will leverage two Event Listeners, one for FailedVerificationCredentialManagerEvent and one for SuccessfulVerificationCredentialManagerEvent.

When the FailedVerificationCredentialManagerEvent event triggers, a counter tied to the user is incremented and persisted in a data store. When the counter reaches a configurable threshold (maxLogin), a timestamp value of current time + a configurable lockout time is stored 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 timestamp will be updated.

If a SuccessfulVerificationCredentialManagerEvent is triggered, the timestamp will be set to 0, and the failed login counter will be set to 0, effectively resetting the lockout. This event only triggers if the user credentials are correct and the stored lockout timer has passed.

The Credential Query in the Data Source used by the Credential Manager needs to be updated to also read the timestamp. 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, the lockout counter and time stamp could be stored in different data sources.

If the default Curity Identity Server database schema is used along with the accounts table to hold account information, the lockout counter and time stamp can be stored there. This will allow for a more optimized credential query since SQL JOIN can be avoided.

If the default accounts table is not used, it is possible to store this information in the buckets table instead. However, this forces the credential query to perform a 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

The Credential Query configured on the Data Source (used by the Credential Manager that the Authenticator is using) must be updated. Do so here: FacilitiesData Sourcesdata source usedCredential 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.

Join our Newsletter

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

Start Free Trial

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

Start Free Trial