Keycloak Custom message on user temporary lock

I am using Kyecloak:12.0.4, and have enabled Brute force attack for my realm. Now whenever user provides wrong credentials for 3 times user will be locked temporarily.

But still user will see “Invalid username/password”.

But still i want to show user that his account has been locked.

Is there any way to customize this message?

I tried doing this by adding message in custom keycloak theme as below:

location: themes\adminlte\login\messages\messages_en.properties

accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later.

1 Like

Anyone has resolved the above said issue ?
I would like to send an email to the user saying “your account is locked, try after 15 mins” . How can I implement it ?

Thanks,
Kabi

Seems a bug in Keycloak backend. I fixed it and refer the workaround below,
Step 1:
Download Keycloak backend source code from Tags · keycloak/keycloak · GitHub (make sure to download your server version)

Step 2:
Make changes on two methods in org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator class in services module,

protected Response challenge(AuthenticationFlowContext context, String error) {
        LoginFormsProvider form = context.form()
                .setExecution(context.getExecution().getId());

        if (error != null) {
            if(error == Messages.ACCOUNT_TEMPORARILY_DISABLED){
                form.setError(error, context.getRealm().getWaitIncrementSeconds() / 60);
            } else {
                form.setError(error);
            }
        }
        return createLoginForm(form);
    }
    protected String tempDisabledError() {
        return Messages.ACCOUNT_TEMPORARILY_DISABLED;
    }

Step 3:
Compile the …/service module and replace the keycloak-services-<>.jar file in server location (.\keycloak-<>\modules\system\layers\keycloak\org\keycloak\keycloak-services\main)

Step 4:
Restart the Keycloak service
(E.g. C:\dev\opt\sso\keycloak-8.0.1\bin\standalone.bat -Djboss.socket.binding.port-offset=100)

Step 5:
Change the message accountTemporarilyDisabledMessage base on your requirement in theme location, themes\adminlte\login\messages\messages_en.properties

Message should be like this,
accountTemporarilyDisabledMessage=Your account is locked due to the multiple invalid login attempts, Please try after {0} mins.

Note: You can cnage the maximum login attempts and Max wating time via Keycloak admin console path,
Realm Settings >> Security Defenses >> Brute Force Detections
Parameters: Max Login Failures and Wait Increment

This works for me.

Enjoy Keycloak :slight_smile:
-Nandika

4 Likes

Hi @Nandika, that’s amazing that you found a bug and solved it by yourself.

Is this bug already registered on Keycloak issue tracker?

Would you mind to create a PR to fix this issue to anyone else in the next release? And also be praised for your contribution :slight_smile: ?

Thank you

1 Like

Thanks @erickmoreno

Refer to one of the related tickets https://issues.redhat.com/browse/KEYCLOAK-8013

But the Keycloak default intention does not tally with the actual user requirements :slight_smile:

This is really helpful. I am very new to keycloak. This helps me to change default behavior of services in all keycloak libraries.

1 Like

Anyone please let me know what are the classes and methods need to change for blocking account based on number of retry attempt. I know this can be implemented using GUI brute force detection but need to do it in java code.

You can try adding a new Custom authentication flow. This video might help Keycloak: Custom Authentication Flows - YouTube

1 Like

This is really helpful. thank you so much.

I have to send email when account is locked after 3 wrong password attempts.

So I have added below code inside DefaultBruteForceProtector failure method.

    ```

DefaultEmailSenderProvider senderProvider = new DefaultEmailSenderProvider(session);
UserModel user1 = session.users().getUserById(realm, userId);
try {
senderProvider.send(
session.getContext().getRealm().getSmtpConfig(),
user1,
“test”,
“body test”,
“html test”
);
} catch (EmailException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}





public void failure(KeycloakSession session, LoginEvent event) {
logger.debug(“failure”);
logger.info(“failure”);
RealmModel realm = getRealmModel(session, event);
logFailure(event);

    String userId = event.userId;

    UserLoginFailureModel userLoginFailure = getUserModel(session, event);
    if (userLoginFailure == null) {
        userLoginFailure = session.loginFailures().addUserLoginFailure(realm, userId);
    }
    userLoginFailure.setLastIPFailure(event.ip);
    long currentTime = Time.currentTimeMillis();
    long last = userLoginFailure.getLastFailure();
    long deltaTime = 0;
    if (last > 0) {
        deltaTime = currentTime - last;
    }
    userLoginFailure.setLastFailure(currentTime);

    DefaultEmailSenderProvider senderProvider = new DefaultEmailSenderProvider(session);
    UserModel user1 = session.users().getUserById(realm, userId);
    try {
        senderProvider.send(
                session.getContext().getRealm().getSmtpConfig(),
                user1,
                "test",
                "body test",
                "html test"
        );
    } catch (EmailException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }


    if(realm.isPermanentLockout()) {
        userLoginFailure.incrementFailures();
        logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());
        logger.infov("new num failures: {0}", userLoginFailure.getNumFailures());

        if(userLoginFailure.getNumFailures() == realm.getFailureFactor()) {
            UserModel user = session.users().getUserById(realm, userId);
            if (user == null) {
                return;
            }
            logger.debugv("user {0} locked permanently due to too many login attempts", user.getUsername());
            logger.infov("user {0} locked permanently due to too many login attempts", user.getUsername());
            user.setEnabled(false);
            user.setSingleAttribute(DISABLED_REASON, DISABLED_BY_PERMANENT_LOCKOUT);
            return;
        }

        if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
            logger.debugv("quick login, set min wait seconds");
            int waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
            int notBefore = (int) (currentTime / 1000) + waitSeconds;
            logger.debugv("set notBefore: {0}", notBefore);
            userLoginFailure.setFailedLoginNotBefore(notBefore);
        }
        return;
    }

    if (deltaTime > 0) {
        // if last failure was more than MAX_DELTA clear failures
        if (deltaTime > (long) realm.getMaxDeltaTimeSeconds() * 1000L) {
            userLoginFailure.clearFailures();
        }
    }
    userLoginFailure.incrementFailures();
    logger.debugv("new num failures: {0}", userLoginFailure.getNumFailures());

    int waitSeconds = realm.getWaitIncrementSeconds() *  (userLoginFailure.getNumFailures() / realm.getFailureFactor());
    logger.debugv("waitSeconds: {0}", waitSeconds);
    logger.debugv("deltaTime: {0}", deltaTime);

    if (waitSeconds == 0) {
        if (last > 0 && deltaTime < realm.getQuickLoginCheckMilliSeconds()) {
            logger.debugv("quick login, set min wait seconds");
            waitSeconds = realm.getMinimumQuickLoginWaitSeconds();
        }
    }
    if (waitSeconds > 0) {
        waitSeconds = Math.min(realm.getMaxFailureWaitSeconds(), waitSeconds);
        int notBefore = (int) (currentTime / 1000) + waitSeconds;
        logger.debugv("set notBefore: {0}", notBefore);
        userLoginFailure.setFailedLoginNotBefore(notBefore);
    }
}

But the only issue is 
session.getContext().getRealm() this Realm is null. and can't get Smtp configs to send email. please help me to sort this out.

need to send email when account is locked. wrote a listener and can send emails. but account locking happen after listener called. so can't use listener as well. Need a way to get smtp configs into DefaultBruteForceProtector

Hello, I would like to share with you my solution for this problem.
Personally I prefer not to warn the user that the account is blocked on the login page because it would indicate to the potential attacker that there is an account with this email.

2 Likes

thank you so much but i don’t have access to this github repo. my github username: pgnaleen.
Also I need to get notification from register page into login page. My requirement is redirect register page into login page after user registered successfully. Also need to show notification inside login page after redirection from register page saying “User created successfully”.

Also I need to send email verification to the user after account is locked because 3 invalid attempts. although verify email action is set not sending any emails until i loginto the user.

Is this an SPI or do you deploy the entire source?

Only change the org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator class in service module and compile and deploy.

Just replace the previous JAR file with the new jar at location .\keycloak-\modules\system\layers\keycloak\org\keycloak\keycloak-services\main and restart the service.

1 Like

You are right sir. Thank you.

Will this work for providers? Like freemarkerLoginFormProviders

Technically YES. try it out :slight_smile:

any one knows how to read keycloak *.properties files inside listener or provider.

This issue is still exist in 19. Are we supposed to write the code and deploy ourselves?

Yes, as the current behaviour is a security feature.

1 Like

@pgnaleen, can you share which approach worked well for you? I am also looking to send an email after three wrong log in attempts.