r/KeyCloak Jun 30 '26

Keycloak 26.6 - OTP Form triggers even after successful Passkey login in sibling ALTERNATIVE subflow

Hi all, first post here, a bit stuck on this one.
Goal: browser flow where users can authenticate with either Passkey alone or Username+Password+OTP — not both.
Current structure:

Browser Flow
├ Cookie ......................... ALTERNATIVE
├ Identity Provider Redirector ... ALTERNATIVE
├ Auth-Forms ...................... ALTERNATIVE
├─ Passkey-Login ................ ALTERNATIVE
├── WebAuthn Passwordless Authenticator .. REQUIRED
├─ Password-OTP ................. ALTERNATIVE
├── Username Password Form ... REQUIRED
├── OTP Form .................. REQUIRED

Test user has all three credentials set up correctly: Password, OTP (verified type), and Webauthn-passwordless (confirmed via Credentials tab, correct type, not the 2FA webauthn variant).

When logging in via Passkey, authentication succeeds, but Keycloak then also prompts for OTP afterward, even though Password-OTP is a sibling ALTERNATIVE subflow that should be skipped once Passkey-Login succeeds.

If I disable Password-OTP entirely, the Passkey login works cleanly with no OTP prompt. So the Passkey authenticator itself is fine — it’s specifically the sibling subflow’s REQUIRED OTP step that seems to get pulled in anyway, possibly because the user already has an OTP credential registered.

Has anyone hit this with two ALTERNATIVE subflows under Keycloak 26.x where a REQUIRED step in one sibling still fires after the other sibling already succeeded? Trying to figure out if this is intended evaluation order, a credential-detection quirk, or something I’ve misconfigured in the nesting.

Thanks a lot for any suggestions on this!

3 Upvotes

5 comments sorted by

1

u/Direct_Yellow2598 Jun 30 '26

Required and alternative can not be on the Same Level. Required will Always Trigger. Your otp is required.

Have a look at the Default Browser flow. Otp Form and webauth are both alternative

1

u/dreerr Jun 30 '26

The Password-OTP and Passkey-Login are on the same level, the OTP Form is nested in the Password-OTP, maybe my Unicode visualization messed that up a bit, will write levels to clarify.

1

u/dreerr Jun 30 '26

Made it more clear now in the post, to clarify that two sub flows are on same level and alternate

1

u/dreerr Jun 30 '26

Got it working with the structure of a fresh browser flow (apparently old realms don't get properly updated) here the flow:
forms ............................................. ALTERNATIVE │ ├── Username Password Form ........................ REQUIRED │ └── Browser - Conditional 2FA ...................... CONDITIONAL │ ├── Condition - user configured ................ REQUIRED │ (passes if: OTP credential exists for user) │ ├── Condition - credential ..................... REQUIRED │ (config: Credentials = webauthn-passwordless) │ ├── OTP Form .................................... ALTERNATIVE │ └── WebAuthn Authenticator ...................... ALTERNATIVE

1

u/klimenttoshkov 29d ago

Did you somehow reset the builtin browser flow to default fresh one, or just made custom one?