March 19, 2025

Locked vs. Disabled Users: Understanding the Difference and Implementing Secure Account Lockout Mechanism

Introduction

In modern authentication systems, protecting user accounts from unauthorized access is crucial. Two common mechanisms to prevent unauthorized access are locked users and disabled users. Understanding the difference between them and implementing a robust strategy to block users after multiple failed login attempts while allowing them to regain access securely is essential for maintaining both security and user experience.

Locked Users vs. Disabled Users

Locked Users

A locked user is temporarily restricted from accessing their account due to security policies, such as multiple failed login attempts. The lockout period usually lasts for a predefined time or until the user takes a recovery action.

  • Temporary restriction
  • Can be unlocked after a certain time or by resetting the password
  • Used to protect against brute-force attacks
  • Account remains valid

Disabled Users

A disabled user is permanently restricted from accessing their account unless manually re-enabled by an administrator or through a specific process.

  • Permanent restriction until manually reactivated
  • Used for security concerns, policy violations, or account closures
  • User cannot regain access without admin intervention
  • Account may be considered inactive or banned

Enabled Users

An enabled user is an account that is active and can log in without restrictions unless specific security policies trigger a lockout or disablement.

  • Active account status
  • User can access all authorized resources
  • Can be affected by security policies, such as lockout rules

Goal: Implementing Secure Account Lockout in Keycloak

To enhance security, we aim to implement a temporary user lockout mechanism after a certain number of failed login attempts, ensuring unauthorized access is prevented while allowing legitimate users to regain access securely.

Configuring Keycloak Lockout Policies

In Keycloak, you can configure account lockout settings under the Authentication section.

Keycloak Lockout Parameters

  • Max Login Failures: Number of failed login attempts before the user is locked (e.g., 2).
  • Permanent Lockout: If enabled, the user is locked permanently until manually unlocked.
  • Wait Increment: The time delay before allowing another login attempt (set to 0 for no delay).
  • Max Wait: Maximum wait time before the user can retry login (e.g., 15 minutes).
  • Failure Reset Time: Duration after which failed attempts are reset (e.g., 15 hours).
  • Quick Login Check Milliseconds: The minimum time to check for quick successive login failures (e.g., 977ms).
  • Minimum Quick Login Wait: Minimum wait time before the system processes the next login attempt (e.g., 15 seconds).

Internal Working of Keycloak Lockout Mechanism

Keycloak tracks login failures using its Event Listener SPI, which records authentication events. The UserModel stores failed attempts, and the system enforces lockout based on these values.

Keycloak Classes Involved in Lockout

  1. org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator - Handles login authentication and failed attempts tracking.
  2. org.keycloak.models.UserModel - Stores user attributes, including failed login attempts.
  3. org.keycloak.services.managers.AuthenticationManager - Enforces lockout policies and authentication flows.
  4. org.keycloak.authentication.authenticators.directgrant.ValidatePassword - Validates passwords and increments failure count.
  5. org.keycloak.events.EventBuilder - Logs authentication failures and successes.

Extending Keycloak Lockout Mechanism

To customize the lockout logic, you can extend AbstractUsernameFormAuthenticator and override the authentication logic.

Custom Lockout Provider Example:

public class CustomLockoutAuthenticator extends AbstractUsernameFormAuthenticator {
    private static final String FAILED_ATTEMPTS = "failedLoginAttempts";
    private static final String LOCKOUT_EXPIRY = "lockoutExpiryTime";

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        UserModel user = context.getUser();
        int attempts = user.getAttributeStream(FAILED_ATTEMPTS)
                .findFirst().map(Integer::parseInt).orElse(0);
        long expiry = user.getAttributeStream(LOCKOUT_EXPIRY)
                .findFirst().map(Long::parseLong).orElse(0L);

        if (System.currentTimeMillis() < expiry) {
            context.failure(AuthenticationFlowError.USER_TEMPORARILY_DISABLED);
            return;
        }

        context.success();
    }

    public void loginFailed(UserModel user) {
        int attempts = user.getAttributeStream(FAILED_ATTEMPTS)
                .findFirst().map(Integer::parseInt).orElse(0) + 1;
        user.setSingleAttribute(FAILED_ATTEMPTS, String.valueOf(attempts));

        if (attempts >= 5) {
            user.setSingleAttribute(LOCKOUT_EXPIRY, 
                String.valueOf(System.currentTimeMillis() + 15 * 60 * 1000));
        }
    }
}

Managing Lockout Time in the Database

The locked time can be stored in the database using the USER_ENTITY table's attributes.

  • Lockout Expiry Time: A timestamp indicating when the user can log in again.
  • Failed Login Attempts: Counter for tracking failed attempts.

Calculating Remaining Lockout Time

To display the remaining time to the user:

long expiryTime = Long.parseLong(user.getFirstAttribute("lockoutExpiryTime"));
long remainingTime = expiryTime - System.currentTimeMillis();
if (remainingTime > 0) {
    long minutes = TimeUnit.MILLISECONDS.toMinutes(remainingTime);
    System.out.println("Your account is locked. Try again in " + minutes + " minutes.");
}

Which Table Stores Max Wait and Other Parameters?

The REALM table in Keycloak stores:

  • maxLoginFailures
  • waitIncrementSeconds
  • maxWaitSeconds
  • failureResetTimeSeconds

These values can be retrieved in an event listener for authentication events.

Event Triggered for Wrong Login Attempts

  • EventType.LOGIN_ERROR: Triggered when a login attempt fails.

Sending Email After X Failed Attempts

To send an email after multiple failures:

if (failedAttempts >= maxLoginFailures) {
    eventBuilder.event(EventType.SEND_RESET_PASSWORD)
        .user(user)
        .realm(realm)
        .success();
    sendLockoutEmail(user);
}

Conclusion

Implementing a secure account lockout mechanism in Keycloak enhances security while maintaining a user-friendly experience. By configuring temporary locks, custom messages, and extending Keycloak providers, we can effectively protect user accounts from unauthorized access while allowing legitimate users to regain access securely.