2FA forced only for specific URLs

518 views
Skip to first unread message

Juan Pablo Gardella

unread,
Mar 14, 2021, 3:13:29 PM3/14/21
to Keycloak User
Hi all,

I found the same question without answers, but let me try again. I have an specific requirement (very common in some applications) where for authentication in a web application is used user and password form. But for specific action, it is forced to validate 2FA code to continue.

AFAIK Keycloak supports at authentication 2FA (A means Authentication no Authorization :)), with some conditionals based on role, headers, etc. That flow only happens during authentication and it is fine. But the problem I am facing is to try to use use that flow for authorization too.

Anyone has an idea to how to support that scenario with Keycloak?

Thanks,
Juan

Juan Pablo Gardella

unread,
Mar 14, 2021, 3:38:47 PM3/14/21
to Keycloak User

image.png
Any example about how to define a policy to confirm a user is authenticated by 2FA?

Juan



--
You received this message because you are subscribed to the Google Groups "Keycloak User" group.
To unsubscribe from this group and stop receiving emails from it, send an email to keycloak-use...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/keycloak-user/50e8b07b-1098-4fc4-af65-f1d320bce70en%40googlegroups.com.

Juan Pablo Gardella

unread,
Mar 23, 2021, 6:43:32 PM3/23/21
to Keycloak User
Hi all,

After hours of digging, I found a way, at least, to check OTP code as a rest endpoint that I can use to validate the code in web application. Summary:

1) Add a custom rest endpoint, for example checkotp?otp=value.
2) Relevant piece of code:

  @GET
  @Produces("text/plain; charset=utf-8")
  public String check(@QueryParam("otp") String otp) {
    LOGGER.info("Calling using otp={}", otp);
    if (otp == null || otp.trim().length() != 6) {
      return "NOK";
    }
    final RealmModel realm = session.getContext().getRealm();

    AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateIdentityCookie(
        session, realm);
    if (authResult == null) {
      LOGGER.info("No authenticatedUser");
      return "NOK";
    }
    UserModel authenticatedUser = authResult.getUser();
    if (authenticatedUser == null) {
      LOGGER.info("No authenticatedUser");
      return "NOK";
    }

    final Optional<CredentialProvider<?>> cp = getCredentialProvider(session);
    if (!cp.isPresent()) {
      LOGGER.info("No credentialProvider");
      return "NOK";
    }

    String credentialId = cp.get().getDefaultCredential(session, realm, authenticatedUser).getId();

    boolean valid = ((CredentialInputValidator) cp.get()).isValid(realm, authenticatedUser,
        new UserCredentialModel(credentialId, OTPCredentialModel.TYPE, otp));
    return valid ? "OK" : "NOK";
  }
 
  private static Optional<CredentialProvider<?>> getCredentialProvider(KeycloakSession session) {
    CredentialProvider<?> cp = session.getProvider(CredentialProvider.class, "keycloak-otp");
    if (cp instanceof CredentialInputValidator) {
      return Optional.of(cp);
    }
    return Optional.empty();
  }

3) Deploy it to keycloak onto deployments folder.

Notes: It is very hacky from my perspective because I have to use classes from keycloak-services to make it work. As I could not be able to deploy as a WAR because it does not work, I copy&paste&remove unnecesary code from classes to make it work. The deployment is a simple jar. Some Keycloak expert/advance user maybe know a better way or how to encapsulate it in a policy.

Hope it helps others that need to have some endpoint to validate OTP code. Ideally it should be a policy but at least this is a first version.

Juan

Juan Pablo Gardella

unread,
May 7, 2021, 7:02:50 AM5/7/21
to Keycloak User
The solution I provided is not useful for external applications. Instead of retrieving user id from cookie, I externalize it.

  @GET
  @Produces("text/plain; charset=utf-8")
  public String check(@QueryParam("otp") String otp, @QueryParam("userId") String userId) {
    LOGGER.info("Calling using otp={}, userId={}", otp, userId);

    if (otp == null || otp.trim().length() != 6) {
      LOGGER.info("OTP is empty");
      return "NOK";
    }
    if (userId == null || userId.trim().isEmpty()) {
      LOGGER.info("userId is empty");

      return "NOK";
    }

    final Optional<CredentialProvider<?>> cp = getCredentialProvider(session);
    if (!cp.isPresent()) {
      LOGGER.info("No credentialProvider");
      return "NOK";
    }

    final RealmModel realm = session.getContext().getRealm();
    LOGGER.info("Realm selected from session {}", realm.getName());

    UserModel user = session.userStorageManager().getUserById(userId, realm);

    if (user == null) {
      LOGGER.info("User '{}' not found", userId);
      return "NOK";
    }

    String credentialId = cp.get().getDefaultCredential(session, realm, user).getId();

    boolean valid = ((CredentialInputValidator) cp.get()).isValid(realm, user,

        new UserCredentialModel(credentialId, OTPCredentialModel.TYPE, otp));
    return valid ? "OK" : "NOK";
  }

Hope it helps others to validate OTP code. For example for action where a user requires to confirm the OTP code before some action.

Juan

Thomas Darimont

unread,
May 7, 2021, 7:12:56 AM5/7/21
to Juan Pablo Gardella, Keycloak User
Be careful with this!

If this is an unprotected endpoint then attackers might be able to leverage this to gain access to user accounts with weak or leaked passwords by brute-forcing otp codes.

This can also be used to detect if a user with a given id exists or not.

It might make sense to only expose this endpoint to internal applications, and require a valid (Service-Account) access-token to invoke that endpoint.

Cheers,
Thomas


Juan Pablo Gardella

unread,
May 7, 2021, 9:11:07 AM5/7/21
to Thomas Darimont, Keycloak User
Thanks Thomas, yes the endpoint should not be exposed. Ideally other mechanisms should be available, but I am still unable to find one to check OTP code.

Juan
Reply all
Reply to author
Forward
0 new messages