Manage the CCCHH browser authentication flow as YAML
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.
This commit is contained in:
parent
4832e5da5d
commit
32b5f147bc
9 changed files with 616 additions and 0 deletions
114
roles/keycloak_auth_flow/README.md
Normal file
114
roles/keycloak_auth_flow/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# `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
|
||||
```
|
||||
Loading…
Reference in a new issue