Update to new manually created flow
This commit is contained in:
parent
32b5f147bc
commit
18b01045e5
3 changed files with 31 additions and 91 deletions
|
|
@ -1,62 +1,31 @@
|
||||||
---
|
---
|
||||||
realm: ccchh
|
realm: ccchh
|
||||||
alias: browser passkey and token
|
alias: passkey and 2FA
|
||||||
description: passkey or (password and (TOTP or token))
|
description: Login flow with passkey and proper 2FA handling.
|
||||||
providerId: basic-flow
|
providerId: basic-flow
|
||||||
|
authenticationExecutions:
|
||||||
|
- subFlow: pk2fa-login
|
||||||
|
requirement: REQUIRED
|
||||||
authenticationExecutions:
|
authenticationExecutions:
|
||||||
- providerId: auth-cookie
|
- providerId: auth-cookie
|
||||||
requirement: ALTERNATIVE
|
requirement: ALTERNATIVE
|
||||||
- providerId: auth-spnego
|
- subFlow: pk2fa-authentication
|
||||||
requirement: DISABLED
|
|
||||||
- providerId: identity-provider-redirector
|
|
||||||
requirement: ALTERNATIVE
|
|
||||||
- subFlow: browser passkey and token browser passkey Organization
|
|
||||||
requirement: DISABLED
|
|
||||||
authenticationExecutions:
|
|
||||||
- subFlow: browser passkey and token browser passkey Browser - Conditional Organization
|
|
||||||
requirement: CONDITIONAL
|
|
||||||
authenticationExecutions:
|
|
||||||
- providerId: conditional-user-configured
|
|
||||||
requirement: REQUIRED
|
|
||||||
- providerId: organization
|
|
||||||
requirement: ALTERNATIVE
|
|
||||||
- subFlow: browser passkey and token browser passkey forms
|
|
||||||
requirement: ALTERNATIVE
|
|
||||||
authenticationExecutions:
|
|
||||||
- subFlow: browser passkey and token Passkey or Password
|
|
||||||
requirement: REQUIRED
|
|
||||||
authenticationExecutions:
|
|
||||||
- providerId: webauthn-authenticator-passwordless
|
|
||||||
requirement: ALTERNATIVE
|
|
||||||
- subFlow: browser passkey and token Password + OTP
|
|
||||||
requirement: ALTERNATIVE
|
requirement: ALTERNATIVE
|
||||||
authenticationExecutions:
|
authenticationExecutions:
|
||||||
- providerId: auth-username-password-form
|
- providerId: auth-username-password-form
|
||||||
requirement: REQUIRED
|
requirement: REQUIRED
|
||||||
- subFlow: browser passkey and token Conditional OTP
|
- subFlow: pk2fa-conditional-2fa
|
||||||
requirement: CONDITIONAL
|
requirement: CONDITIONAL
|
||||||
authenticationExecutions:
|
authenticationExecutions:
|
||||||
- providerId: conditional-credential
|
- providerId: conditional-credential
|
||||||
requirement: REQUIRED
|
requirement: REQUIRED
|
||||||
authenticationConfig:
|
authenticationConfig:
|
||||||
alias: browser passkey and token not passkey
|
alias: pk2fa-conditional-2fa-passwordless-conditional
|
||||||
config:
|
|
||||||
credentials: webauthn-passwordless
|
|
||||||
included: 'false'
|
|
||||||
- providerId: conditional-user-configured
|
|
||||||
requirement: REQUIRED
|
|
||||||
- providerId: auth-otp-form
|
|
||||||
requirement: REQUIRED
|
|
||||||
- subFlow: WebAuthn Authenticator if configured for the user
|
|
||||||
requirement: CONDITIONAL
|
|
||||||
authenticationExecutions:
|
|
||||||
- providerId: conditional-credential
|
|
||||||
requirement: REQUIRED
|
|
||||||
authenticationConfig:
|
|
||||||
alias: webauthn-passwordless
|
|
||||||
config:
|
config:
|
||||||
credentials: webauthn-passwordless
|
credentials: webauthn-passwordless
|
||||||
- providerId: conditional-user-configured
|
- providerId: conditional-user-configured
|
||||||
requirement: REQUIRED
|
requirement: REQUIRED
|
||||||
- providerId: webauthn-authenticator
|
- providerId: webauthn-authenticator
|
||||||
requirement: REQUIRED
|
requirement: ALTERNATIVE
|
||||||
|
- providerId: auth-otp-form
|
||||||
|
requirement: ALTERNATIVE
|
||||||
|
|
|
||||||
|
|
@ -57,13 +57,6 @@ checks each flow definition before it's ever sent to the server.
|
||||||
sub-flow's own `requirement` is literally `CONDITIONAL`. See the Keycloak Server Admin Guide,
|
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):
|
["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."
|
"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
|
- **`conditional-credential` config**: requires an `authenticationConfig` with a non-empty
|
||||||
`credentials` entry, and `included` (if set) must be the string `"true"` or `"false"` — Keycloak
|
`credentials` entry, and `included` (if set) must be the string `"true"` or `"false"` — Keycloak
|
||||||
stores authenticator config values as strings. See
|
stores authenticator config values as strings. See
|
||||||
|
|
@ -80,8 +73,8 @@ checks each flow definition before it's ever sent to the server.
|
||||||
execution ended up under a `Conditional` sub-flow.
|
execution ended up under a `Conditional` sub-flow.
|
||||||
- **`conditional-credential` `included` semantics reminder**: restates what the configured
|
- **`conditional-credential` `included` semantics reminder**: restates what the configured
|
||||||
`included` value actually means (`true` → condition is true when *any* listed credential was
|
`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
|
used; `false` or unset → true when *none* were used, since Keycloak parses a missing value as
|
||||||
get backwards — see the same
|
`false`). 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)
|
[conditions.adoc](https://github.com/keycloak/keycloak/blob/main/docs/documentation/server_admin/topics/authentication/conditions.adoc)
|
||||||
reference as above.
|
reference as above.
|
||||||
- **`auth-conditional-otp-form` with no config**: unlike the plain `auth-otp-form`, `Conditional
|
- **`auth-conditional-otp-form` with no config**: unlike the plain `auth-otp-form`, `Conditional
|
||||||
|
|
@ -101,7 +94,7 @@ checks each flow definition before it's ever sent to the server.
|
||||||
```yaml
|
```yaml
|
||||||
keycloak_auth_flow__flows:
|
keycloak_auth_flow__flows:
|
||||||
- realm: ccchh
|
- realm: ccchh
|
||||||
alias: browser passkey and token
|
alias: passkey and 2FA
|
||||||
providerId: basic-flow
|
providerId: basic-flow
|
||||||
authenticationExecutions:
|
authenticationExecutions:
|
||||||
- providerId: auth-cookie
|
- providerId: auth-cookie
|
||||||
|
|
|
||||||
|
|
@ -128,12 +128,6 @@ ALLOWED_NODE_KEYS = {
|
||||||
}
|
}
|
||||||
MAX_SUBFLOW_DEPTH = 10
|
MAX_SUBFLOW_DEPTH = 10
|
||||||
|
|
||||||
# providerIds relevant to the sibling-condition bug tracked as keycloak/keycloak#29515: mixing
|
|
||||||
# these two under one shared 'conditional-user-configured' check breaks login for users with
|
|
||||||
# neither credential configured.
|
|
||||||
OTP_PROVIDER_IDS = {"auth-otp-form", "auth-conditional-otp-form"}
|
|
||||||
WEBAUTHN_PROVIDER_IDS = {"webauthn-authenticator", "webauthn-authenticator-passwordless"}
|
|
||||||
|
|
||||||
|
|
||||||
class FlowValidationError(Exception):
|
class FlowValidationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
@ -165,8 +159,9 @@ def validate_authentication_config(auth_config, path, provider_id, warnings):
|
||||||
f"(got {included!r}) — Keycloak stores authenticator config values as strings"
|
f"(got {included!r}) — Keycloak stores authenticator config values as strings"
|
||||||
)
|
)
|
||||||
# The included=true/false semantics are unlabeled in the Keycloak admin console and easy to
|
# The included=true/false semantics are unlabeled in the Keycloak admin console and easy to
|
||||||
# get backwards, so restate them every time as a standing reminder.
|
# get backwards, so restate them every time as a standing reminder. Keycloak reads the
|
||||||
if included == "false":
|
# value with Boolean.parseBoolean(), so an unset 'included' behaves like "false".
|
||||||
|
if included != "true":
|
||||||
meaning = f"TRUE when NONE of [{credentials}] were used (inverted)"
|
meaning = f"TRUE when NONE of [{credentials}] were used (inverted)"
|
||||||
else:
|
else:
|
||||||
meaning = f"TRUE when ANY of [{credentials}] was used"
|
meaning = f"TRUE when ANY of [{credentials}] was used"
|
||||||
|
|
@ -231,9 +226,9 @@ def validate_node(node, path, depth, seen_subflow_names, seen_provider_ids, warn
|
||||||
if not isinstance(children, list):
|
if not isinstance(children, list):
|
||||||
raise FlowValidationError(f"{path}.authenticationExecutions: must be a list")
|
raise FlowValidationError(f"{path}.authenticationExecutions: must be a list")
|
||||||
|
|
||||||
# Direct-child providerIds, used by the two checks below. Only *direct* children count:
|
# Direct-child providerIds, used by the check below. Only *direct* children count:
|
||||||
# Keycloak's "Condition executions can only be contained in Conditional subflow" rule, and
|
# Keycloak's "Condition executions can only be contained in Conditional subflow" rule
|
||||||
# the sibling-condition bug, both concern immediate siblings, not deeper descendants.
|
# concerns immediate children, not deeper descendants.
|
||||||
direct_provider_ids = {
|
direct_provider_ids = {
|
||||||
child.get("providerId")
|
child.get("providerId")
|
||||||
for child in children
|
for child in children
|
||||||
|
|
@ -252,23 +247,6 @@ def validate_node(node, path, depth, seen_subflow_names, seen_provider_ids, warn
|
||||||
"never fires)"
|
"never fires)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gotcha: keycloak/keycloak#29515 — an OTP provider and a WebAuthn provider as siblings
|
|
||||||
# under one shared 'conditional-user-configured' check breaks login for users with neither
|
|
||||||
# credential configured. Give each credential type its own nested Conditional subflow with
|
|
||||||
# its own 'conditional-user-configured' check instead.
|
|
||||||
if (
|
|
||||||
"conditional-user-configured" in direct_provider_ids
|
|
||||||
and direct_provider_ids & OTP_PROVIDER_IDS
|
|
||||||
and direct_provider_ids & WEBAUTHN_PROVIDER_IDS
|
|
||||||
):
|
|
||||||
raise FlowValidationError(
|
|
||||||
f"{path}: sub-flow {name!r} mixes an OTP provider and a WebAuthn provider as "
|
|
||||||
"siblings under one shared 'conditional-user-configured' check — this hits "
|
|
||||||
"keycloak/keycloak#29515: users with neither credential configured get a generic "
|
|
||||||
"login error instead of the check being skipped. Give each credential type its own "
|
|
||||||
"nested Conditional subflow with its own 'conditional-user-configured' check."
|
|
||||||
)
|
|
||||||
|
|
||||||
for index, child in enumerate(children):
|
for index, child in enumerate(children):
|
||||||
validate_node(
|
validate_node(
|
||||||
child,
|
child,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue