Adds a keycloak_auth_flow role that locally validates flow definitions (structure, nesting depth, known provider IDs, and a set of Keycloak authentication-flow gotchas: conditions silently ignored outside a Conditional subflow, keycloak/keycloak#29515's OTP+WebAuthn sibling bug, conditional-credential config shape/semantics, and more) before deploying them idempotently via middleware_automation.keycloak.keycloak_authentication_v2. Includes the CCCHH realm's actual "browser passkey and token" flow (the realm's real browserFlow binding, not the untouched built-in "browser" flow), exported from the live server so it's now reviewable and redeployable from this repo instead of only editable in the Keycloak admin console.
114 lines
7.2 KiB
Markdown
114 lines
7.2 KiB
Markdown
# `keycloak_auth_flow` role
|
|
|
|
Manages Keycloak authentication flows (for example a realm's `browser` flow) as YAML checked
|
|
into this repo, instead of hand-editing them in the Keycloak admin console.
|
|
|
|
Each flow is validated locally (structure, nesting depth, known provider IDs) with the custom
|
|
[`keycloak_auth_flow_lint`](library/keycloak_auth_flow_lint.py) module before being deployed with
|
|
`middleware_automation.keycloak.keycloak_authentication_v2`, which applies changes idempotently
|
|
via a "safe swap" so the realm is never left without a working flow mid-update.
|
|
|
|
Both tasks run against `keycloak_auth_flow__admin_url`, which defaults to the local
|
|
`http://127.0.0.1:8080` — i.e. this role is meant to run on the Keycloak host itself, talking to
|
|
the Keycloak container directly, since the public admin hostname is IP-restricted.
|
|
|
|
## Required Arguments
|
|
|
|
- `keycloak_auth_flow__admin_password`: Password of `keycloak_auth_flow__admin_username` (an
|
|
admin in the `master` realm).
|
|
|
|
## Optional Arguments
|
|
|
|
- `keycloak_auth_flow__flows`: List of flow definitions to deploy. Defaults to `[]`. Each item:
|
|
- `realm`: Realm the flow belongs to.
|
|
- `alias`: Name of the flow.
|
|
- `providerId`: `basic-flow` (default) or `client-flow`.
|
|
- `description`: Optional human-readable description.
|
|
- `authenticationExecutions`: Nested list describing the flow's steps, in the shape documented
|
|
by `middleware_automation.keycloak.keycloak_authentication_v2` — each item is either an
|
|
execution (`providerId`, `requirement`, optionally `authenticationConfig`) or a sub-flow
|
|
(`subFlow`, `requirement`, optionally `subFlowType`, and a nested `authenticationExecutions`).
|
|
Up to 10 levels of sub-flow nesting are supported.
|
|
- `keycloak_auth_flow__admin_url`: Defaults to `http://127.0.0.1:8080`.
|
|
- `keycloak_auth_flow__admin_username`: Defaults to `admin`.
|
|
|
|
## Validation checks (`keycloak_auth_flow_lint`)
|
|
|
|
The lint module runs entirely offline (no Keycloak credentials or network access needed) and
|
|
checks each flow definition before it's ever sent to the server.
|
|
|
|
### Errors (fail the run)
|
|
|
|
- **Node shape**: every entry in `authenticationExecutions` must be a mapping with no unknown
|
|
keys, exactly one of `providerId` (an execution) or `subFlow` (a sub-flow), and a `requirement`
|
|
that is one of `REQUIRED`/`ALTERNATIVE`/`DISABLED`/`CONDITIONAL`. A sub-flow's `subFlowType`
|
|
(when given) must be `basic-flow` or `form-flow`, its name must be a non-empty string unique
|
|
within the flow, and it must define at least one child execution. These mirror the shape
|
|
`middleware_automation.keycloak.keycloak_authentication_v2` itself expects — see its
|
|
[module source](https://github.com/ansible-middleware/keycloak/blob/3.0.11/plugins/modules/keycloak_authentication_v2.py).
|
|
- **Nesting depth**: at most 10 levels of sub-flow nesting, matching the limit built into
|
|
`keycloak_authentication_v2` (see the module source linked above; the depth was raised from 4
|
|
to 10 in [ansible-middleware/keycloak#376](https://github.com/ansible-middleware/keycloak/pull/376)).
|
|
- **`authenticationConfig` shape**: when present, must be a mapping with only `alias` (non-empty
|
|
string) and `config` (a mapping) — again mirrors what
|
|
`keycloak_authentication_v2` accepts.
|
|
- **Condition execution outside a `CONDITIONAL` sub-flow**: a `conditional-*` execution is
|
|
silently ignored — no error, the condition just never fires — unless its immediate parent
|
|
sub-flow's own `requirement` is literally `CONDITIONAL`. See the 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."
|
|
- **OTP + WebAuthn siblings under one shared `conditional-user-configured`**: this hits
|
|
[keycloak/keycloak#29515](https://github.com/keycloak/keycloak/issues/29515) — users with
|
|
neither credential configured get a generic login error instead of the check being skipped
|
|
(also discussed in
|
|
[keycloak/keycloak#14988](https://github.com/keycloak/keycloak/discussions/14988)). The fix is
|
|
to give each credential type its own nested `CONDITIONAL` sub-flow with its own
|
|
`conditional-user-configured` check.
|
|
- **`conditional-credential` config**: requires an `authenticationConfig` with a non-empty
|
|
`credentials` entry, and `included` (if set) must be the string `"true"` or `"false"` — Keycloak
|
|
stores authenticator config values as strings. See
|
|
[`ConditionalCredentialAuthenticatorFactory` javadoc](https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/authentication/authenticators/conditional/ConditionalCredentialAuthenticatorFactory.html)
|
|
and [conditions.adoc](https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/authentication/conditions.adoc).
|
|
|
|
### Warnings (non-fatal)
|
|
|
|
- **Unrecognized `providerId`**: not in the module's built-in list of known Keycloak provider
|
|
IDs — could be a typo, or a legitimate custom SPI provider. See the constant
|
|
`KNOWN_PROVIDER_IDS` in the module source for how that list was compiled and how to check or
|
|
extend it (live server query vs. Keycloak's authenticator factory sources).
|
|
- **`CONDITIONAL` requirement on a non-`conditional-*` provider**: usually a sign the wrong
|
|
execution ended up under a `Conditional` sub-flow.
|
|
- **`conditional-credential` `included` semantics reminder**: restates what the configured
|
|
`included` value actually means (`true` → condition is true when *any* listed credential was
|
|
used; `false` → true when *none* were used). This is unlabeled in the admin console and easy to
|
|
get backwards — see the same
|
|
[conditions.adoc](https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/authentication/conditions.adoc)
|
|
reference as above.
|
|
- **`auth-conditional-otp-form` with no config**: unlike the plain `auth-otp-form`, `Conditional
|
|
OTP Form`'s skip/force logic is entirely self-contained (by user attribute, role, header, or
|
|
default) and does *not* inherit a wrapping `Condition-*` check; with no config it defaults to
|
|
always showing the OTP form. See
|
|
[`ConditionalOtpFormAuthenticator.java`](https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java).
|
|
- **Ambiguous passkey execution**: when both `auth-username-password-form` and
|
|
`webauthn-authenticator-passwordless` appear anywhere in the flow. With native passkey support,
|
|
the username/password form can itself complete a full passkey login, so it can be unclear —
|
|
and unverifiable from the admin console — which execution actually authenticated a given
|
|
login. No online source for this one; verify via the `execution=<id>` query parameter during a
|
|
live test, or `DEBUG` logging on `org.keycloak.authentication`.
|
|
|
|
## Example
|
|
|
|
```yaml
|
|
keycloak_auth_flow__flows:
|
|
- realm: ccchh
|
|
alias: browser passkey and token
|
|
providerId: basic-flow
|
|
authenticationExecutions:
|
|
- providerId: auth-cookie
|
|
requirement: ALTERNATIVE
|
|
- subFlow: forms
|
|
requirement: ALTERNATIVE
|
|
authenticationExecutions:
|
|
- providerId: auth-username-password-form
|
|
requirement: REQUIRED
|
|
```
|