Ask user to configure OTP on login when not configured

We’ve been trying to build an authentication flow that supports role-based two-factor authentication, and have come up against the following issue:

From what it looks like to me, there are two things that can prompt a user to configure a OTP authenticator app on login: the “configure OTP” required action, and the “Reset OTP” authentication flow step, which effectively calls that required action.

Problem is: I can only make a conditional subflow that tests if a user is configured, and if yes, then proceed. It seems I cannot make one that negates this (i.e. tests if a user is NOT configured).

Since we’re trying to bind the need for 2FA to a role that not everyone has, I cannot make “configure OTP” a global required action for all users. Instead, the flow should be able to detect if a user has the role (this part already works) and then ensure that the user is made to set up their authenticator (this part doesn’t work yet).

Can this be done at all?

I notice that for the Webauthn 2FA option, this behavior seems to be baked in - as soon as the “Webauthn Authenticator” step is reached, it either asks you for the token you have configured, or if none is configured, it asks you to configure one. Thus you don’t need to do anything in the authentication flow to support it. But since the OTP form does not appear to support the same thing, I was hoping to be able to define the flow in a way that achieves the same end goal.

I think you want a mandatory 2FA flow.

You can change default built-in browser flow and set auth-otp-form from Conditional to Required.

Better, you should copy default browser flow and do this change on the new flow, then you need to bind this new flow as default Browser flow.

If you want support for webauthn also, see this discussion: Create authentication flow with mandatory 2FA with OTP or WebAuthn · keycloak/keycloak · Discussion #14988 · GitHub

Thanks for the info, that link was very interesting - although the example shown does not seem to work for me.

What ultimately solved the problem I asked about here was something else, though…

Turns out, Keycloak ships with two different forms for OTP authentication: the normal “OTP form”, and the “conditional OTP form”.

And among those two, only the latter supports triggering registration when the user doesn’t yet have an authenticator configured. The former does not support this and simply errors out.

We were using the “OTP form” because we didn’t need the additional configuration options that come with the conditional form, and thought to keep things simple. But in this case that was a mistake.

Okay, the more I experiment, the less I understand.

Does anyone have insight why the following flow does not work?

  • cookie: alternative
  • sub-flow: alternative
    • username password form: required
    • sub-flow: conditional
      • condition - user role: required
      • sub-flow: conditional
        • condition - user configured: required
        • conditional otp form: required

Or in other words, (1) ask the user for a password, (2) check IF the user has a specific role, (3) check IF the user has an authenticator app configured, and then ask for the OTP code.

The intention: if the user does not have the role, they can log in without 2FA.
If the user has the role but no authenticator configured, they can also log in without 2FA.
But if both are true, the user is asked for their second factor.

What actually happens: user without the role can log in just fine, but user with the role gets an unhelpful login error (“wrong username or password”) directly after entering the password.

However, if a shortened flow without role-checking is used, it works just fine:

  • cookie: alternative
  • sub-flow: alternative
    • username password form: required
    • sub-flow: conditional
      • condition - user configured: required
      • conditional otp form: required

There are no errors here. But obviously also no role-checking.

Meanwhile, checking only the role but not the configuration state also works:

  • cookie: alternative
  • sub-flow: alternative
    • username password form: required
    • sub-flow: conditional
      • condition - user role: required
      • conditional otp form: required

Again no errors here, and the user with the role is even prompted to set up their authenticator right then and there.

So what is it about these sequential conditional subflows that Keycloak doesn’t like…?

I’m not an expert on flows configuration, but I think you should use Required on your 3rd sub-flow.

I mean, that would get rid of the error, I’m pretty sure.

But it would also no longer check for whether the user has an authenticator configured, right? Because the sub-flow needs to be set as “conditional” in order for a condition block to be evaluated? …At least that’s my limited understanding of that mechanic.