Configure Keycloak Authentication flow from Ansible #148

Open
opened 2026-09-01 19:04:22 +02:00 by stb · 2 comments
Owner

Because the Authentication Flow editor is so horrible, let's try to configure the Browser flow from ansible from a yaml file or similar.

See the image for the flow that implements:

  • Passkey: sufficient to log in
  • Username and Passwort
    • If a TOTP or Webauthn (hardware token) is configured, require it; if not configured, username/password is sufficient
Because the Authentication Flow editor is so horrible, let's try to configure the Browser flow from ansible from a yaml file or similar. See the image for the flow that implements: * Passkey: sufficient to log in * Username and Passwort * If a TOTP or Webauthn (hardware token) is configured, require it; if not configured, username/password is sufficient
Author
Owner

Multiple issues. The attached config now works with (passkey OR (username+password AND (totp OR webauthn authenticator)). Also the time on the VM was wrong.

We really need a way to configure the flow from Ansible.

Claude:

That "fails entirely for users with neither" symptom is a well-known Keycloak gotcha, not a mistake on your end — there's an open bug about it: a user reported adding WebAuthn Authenticator and OTP Form as alternatives in the same conditional subflow, and getting an "invalid username or password" error for anyone without either configured, even though it worked fine with just one of the two. The Condition - user configured check gets confused when it has to evaluate two different credential types side by side in one subflow — which lines up with why your setup worked fine with OTP alone but breaks once WebAuthn joins it.
The reliable workaround: give each credential type its own Condition - user configured check in its own nested Conditional subflow, rather than sharing one check across both. Each condition then only ever has to reason about a single credential type — exactly the pattern that already works for your OTP-only case.

Multiple issues. The attached config now works with (passkey OR (username+password AND (totp OR webauthn authenticator)). Also the time on the VM was wrong. We really need a way to configure the flow from Ansible. Claude: > That "fails entirely for users with neither" symptom is a well-known Keycloak gotcha, not a mistake on your end — there's an open bug about it: a user reported adding WebAuthn Authenticator and OTP Form as alternatives in the same conditional subflow, and getting an "invalid username or password" error for anyone without either configured, even though it worked fine with just one of the two. The Condition - user configured check gets confused when it has to evaluate two different credential types side by side in one subflow — which lines up with why your setup worked fine with OTP alone but breaks once WebAuthn joins it. The reliable workaround: give each credential type its own Condition - user configured check in its own nested Conditional subflow, rather than sharing one check across both. Each condition then only ever has to reason about a single credential type — exactly the pattern that already works for your OTP-only case.
Author
Owner

Claude has this to say about the situation

Keycloak: Passkey login + conditional 2FA — flow configuration notes

Goal

Single browser flow that supports:

  • Full passwordless login via passkey (resident-key WebAuthn), skipping any second factor.
  • Fallback to username + password, with an optional second factor (TOTP and/or WebAuthn security key) that is only requested if the user has one configured.

Keycloak version: 26.4+ (has native Passkeys support).

Final working structure

browser passkey
├─ Cookie                                    Alternative
├─ Kerberos                                  Disabled
├─ Identity Provider Redirector               Alternative
└─ forms                                      Alternative
   └─ Password + OTP                          Required
      ├─ Username Password Form               Required
      │    (handles both password AND passkey login — see Gotcha 6)
      └─ Second Factor Gate                    Conditional
           ├─ Condition - Credential           Required
           │    credentials: webauthn-passwordless, Included: off
           ├─ OTP branch                       Conditional, Alternative
           │    ├─ Condition - user configured  Required
           │    └─ OTP Form                     Required
           └─ WebAuthn branch                  Conditional, Alternative
                ├─ Condition - user configured  Required
                └─ WebAuthn Authenticator       Required

Realm-level setting: Authentication → Policies → WebAuthn Passwordless Policy → Passkeys toggle enabled.

Gotchas / shortcomings encountered

1. Existing realms are not migrated when Keycloak adds new flow steps

Keycloak's passkey feature (26.4) ships a Condition - credential step baked into the default browser flow for newly created realms, plus native conditional UI in the username/password form. Flows are realm data, so a realm created before 26.4 keeps its old flow structure untouched after upgrade — enabling the Passkeys policy toggle alone does not retrofit the skip-2FA logic into an existing flow. It has to be added manually.
Source: Passkeys with Keycloak 26.4 or Newer — describes the manual Condition - Credential addition required for exactly this situation.

2. Condition - steps are inert unless the wrapping subflow's requirement is literally "Conditional"

Alternative, Required, or a plain subflow will not activate nested Condition - executions. Easy to get right structurally but leave the parent subflow's own requirement dropdown on the wrong value (we did, twice) — no error is shown, the conditions are just silently ignored and the wrapped step (e.g. OTP Form) always/never runs.
Source: Keycloak Server Admin Guide, Configuring authentication → Execution requirements — "Condition executions can only be contained in Conditional subflow."

3. Condition - Credential "Included" field semantics are non-obvious and unlabeled in the UI

  • Included = true → condition is true when any listed credential type was used.
  • Included = false → condition is true when none of the listed credential types were used (i.e. inverted/negated).
    No tooltip conveys this in the admin console; got it wrong once (had it inverted, which caused OTP to be skipped for password logins and required for passkey logins — the opposite of intended).
    Source: conditions.adoc, ConditionalCredentialAuthenticatorFactory javadoc.

4. Known bug: mixing OTP Form + WebAuthn Authenticator under one shared Condition - user configured breaks login for users with neither credential configured

Documented, unresolved as of this writing: when OTP Form and WebAuthn Authenticator sit as siblings (Alternative) under a single Condition - user configured check, users who have configured either work fine, but users with neither get a generic "invalid username or password" error instead of the second factor being skipped, because of an AuthenticationFlowException thrown internally rather than the subflow being cleanly skipped.

Workaround used: give each credential type its own Condition - user configured check inside its own nested Conditional subflow (see "OTP branch" / "WebAuthn branch" above), rather than sharing one condition across two credential types. Each condition then only ever reasons about a single credential type, which avoids whatever internal state the shared-condition case corrupts. Not documented anywhere as an official fix — arrived at by generalizing the pattern that already worked for the OTP-only case.

5. Conditional OTP Form and OTP Form are two different authenticators

Conditional OTP Form (ConditionalOtpFormAuthenticator) is a distinct, self-contained authenticator with its own independent skip/force logic (by user attribute, role, HTTP header, or a configured default), separate from the outer flow's Condition - executions. If left unconfigured, its internal fallback is to always show the OTP form — it does not silently skip on its own. Easy to mistake for the plain OTP Form execution when reading a flow tree; worth checking which one is actually present before assuming external Condition - steps are the only thing controlling it.
Source: ConditionalOtpFormAuthenticator.java.

6. Ambiguous which execution actually authenticates a passkey login

With 26.4's native passkey support, Username Password Form itself can complete a full passkey login (via built-in conditional UI/autofill), which means a separately-added WebAuthn Passwordless Authenticator execution elsewhere in the flow can end up dead/unreached without any indication in the admin console. The only reliable way to confirm which execution handled a given login is to watch the execution=<id> query parameter in the login-actions redirect during a live test, or enable DEBUG logging on org.keycloak.authentication and read the flow trace.

Follow-ups / to verify

  • Confirm the standalone WebAuthn Passwordless Authenticator execution (if still present anywhere in the flow) is actually reachable, or remove it if native passkey handling via Username Password Form makes it redundant.
  • Re-test the "neither OTP nor WebAuthn configured" path after the nested-subflow workaround, across a few different accounts, since the underlying bug (#29515) was never confirmed fixed upstream — this is a workaround, not a guarantee.
  • Watch Keycloak release notes for a fix to #29515 / #14988; if merged, the nested-subflow workaround could likely be simplified back to a single shared Condition - user configured.
> Claude has this to say about the situation # Keycloak: Passkey login + conditional 2FA — flow configuration notes ## Goal Single browser flow that supports: - Full passwordless login via passkey (resident-key WebAuthn), **skipping** any second factor. - Fallback to username + password, with an **optional** second factor (TOTP and/or WebAuthn security key) that is only requested if the user has one configured. Keycloak version: 26.4+ (has native Passkeys support). ## Final working structure ``` browser passkey ├─ Cookie Alternative ├─ Kerberos Disabled ├─ Identity Provider Redirector Alternative └─ forms Alternative └─ Password + OTP Required ├─ Username Password Form Required │ (handles both password AND passkey login — see Gotcha 6) └─ Second Factor Gate Conditional ├─ Condition - Credential Required │ credentials: webauthn-passwordless, Included: off ├─ OTP branch Conditional, Alternative │ ├─ Condition - user configured Required │ └─ OTP Form Required └─ WebAuthn branch Conditional, Alternative ├─ Condition - user configured Required └─ WebAuthn Authenticator Required ``` Realm-level setting: **Authentication → Policies → WebAuthn Passwordless Policy → Passkeys** toggle enabled. ## Gotchas / shortcomings encountered ### 1. Existing realms are not migrated when Keycloak adds new flow steps Keycloak's passkey feature (26.4) ships a `Condition - credential` step baked into the *default* browser flow for **newly created** realms, plus native conditional UI in the username/password form. Flows are realm data, so a realm created before 26.4 keeps its old flow structure untouched after upgrade — enabling the `Passkeys` policy toggle alone does **not** retrofit the skip-2FA logic into an existing flow. It has to be added manually. Source: [Passkeys with Keycloak 26.4 or Newer](https://www.open200.com/post/passkeys-keycloak) — describes the manual `Condition - Credential` addition required for exactly this situation. ### 2. `Condition -` steps are inert unless the wrapping subflow's requirement is literally "Conditional" `Alternative`, `Required`, or a plain subflow will not activate nested `Condition -` executions. Easy to get right structurally but leave the parent subflow's own requirement dropdown on the wrong value (we did, twice) — no error is shown, the conditions are just silently ignored and the wrapped step (e.g. OTP Form) always/never runs. Source: Keycloak Server Admin Guide, [Configuring authentication → Execution requirements](https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/22.0/html/server_administration_guide/configuring-authentication_server_administration_guide) — "Condition executions can only be contained in Conditional subflow." ### 3. `Condition - Credential` "Included" field semantics are non-obvious and unlabeled in the UI - `Included = true` → condition is true when **any** listed credential type was used. - `Included = false` → condition is true when **none** of the listed credential types were used (i.e. inverted/negated). No tooltip conveys this in the admin console; got it wrong once (had it inverted, which caused OTP to be skipped for password logins and required for passkey logins — the opposite of intended). Source: [conditions.adoc](https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/authentication/conditions.adoc), [ConditionalCredentialAuthenticatorFactory javadoc](https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/authentication/authenticators/conditional/ConditionalCredentialAuthenticatorFactory.html). ### 4. Known bug: mixing OTP Form + WebAuthn Authenticator under one shared `Condition - user configured` breaks login for users with neither credential configured Documented, unresolved as of this writing: when `OTP Form` and `WebAuthn Authenticator` sit as siblings (Alternative) under a single `Condition - user configured` check, users who have configured **either** work fine, but users with **neither** get a generic "invalid username or password" error instead of the second factor being skipped, because of an `AuthenticationFlowException` thrown internally rather than the subflow being cleanly skipped. - [GitHub issue #29515 — "Conditional flow fails when OTP and Webauthn are on the same flow"](https://github.com/keycloak/keycloak/issues/29515) - [GitHub discussion #14988 — "Create authentication flow with mandatory 2FA with OTP or WebAuthn"](https://github.com/keycloak/keycloak/discussions/14988) (same symptom, community workarounds discussed, no clean fix found) - [Google Groups thread — "Supporting both OTP and WebAuthn for authentication"](https://groups.google.com/g/keycloak-user/c/b-lrakatsPs) (older report of the same class of issue) **Workaround used**: give each credential type its own `Condition - user configured` check inside its own nested Conditional subflow (see "OTP branch" / "WebAuthn branch" above), rather than sharing one condition across two credential types. Each condition then only ever reasons about a single credential type, which avoids whatever internal state the shared-condition case corrupts. Not documented anywhere as an official fix — arrived at by generalizing the pattern that already worked for the OTP-only case. ### 5. `Conditional OTP Form` and `OTP Form` are two different authenticators `Conditional OTP Form` (`ConditionalOtpFormAuthenticator`) is a distinct, self-contained authenticator with its own independent skip/force logic (by user attribute, role, HTTP header, or a configured default), separate from the outer flow's `Condition -` executions. If left unconfigured, its internal fallback is to **always show** the OTP form — it does not silently skip on its own. Easy to mistake for the plain `OTP Form` execution when reading a flow tree; worth checking which one is actually present before assuming external `Condition -` steps are the only thing controlling it. Source: [`ConditionalOtpFormAuthenticator.java`](https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java). ### 6. Ambiguous which execution actually authenticates a passkey login With 26.4's native passkey support, `Username Password Form` itself can complete a full passkey login (via built-in conditional UI/autofill), which means a separately-added `WebAuthn Passwordless Authenticator` execution elsewhere in the flow can end up dead/unreached without any indication in the admin console. The only reliable way to confirm which execution handled a given login is to watch the `execution=<id>` query parameter in the login-actions redirect during a live test, or enable `DEBUG` logging on `org.keycloak.authentication` and read the flow trace. ## Follow-ups / to verify - Confirm the standalone `WebAuthn Passwordless Authenticator` execution (if still present anywhere in the flow) is actually reachable, or remove it if native passkey handling via `Username Password Form` makes it redundant. - Re-test the "neither OTP nor WebAuthn configured" path after the nested-subflow workaround, across a few different accounts, since the underlying bug (#29515) was never confirmed fixed upstream — this is a workaround, not a guarantee. - Watch Keycloak release notes for a fix to #29515 / #14988; if merged, the nested-subflow workaround could likely be simplified back to a single shared `Condition - user configured`.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
CCCHH/ansible-infra#148
No description provided.