diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index 5a49c80b..868f5e36 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -690,16 +690,6 @@ data: title: "Sign in with your passkey" designation: authentication - - id: passkey-passwordless-identification - model: authentik_stages_identification.identificationstage - identifiers: - name: passkey-passwordless-identification - state: present - attrs: - user_fields: - - username - - email - # SEPARATE stage from the step-up one, and deliberately `deny`: # at this point in an authentication flow the user has only been # *identified* (typed a username), not authenticated — `configure` @@ -710,6 +700,10 @@ data: # fully authenticated session. A passkey-less user therefore # bootstraps via password login on auth.renner.uno, then gets the # enrollment prompt on first app access. + # + # Declared BEFORE the identification stage because the latter + # references it via !KeyOf (webauthn_stage), and the blueprint + # importer resolves entries top-to-bottom. - id: passkey-passwordless-webauthn-validation model: authentik_stages_authenticator_validate.authenticatorvalidatestage identifiers: @@ -719,9 +713,31 @@ data: device_classes: - webauthn not_configured_action: deny - last_auth_threshold: seconds=0 + # Small non-zero window so a passkey presented via the + # identification stage's autofill (webauthn_stage below) is + # not challenged a second time by this stage at order 20 in + # the same flow run. The skip is session-scoped, so a fresh + # (attacker) session never inherits it. + last_auth_threshold: minutes=5 webauthn_user_verification: preferred + # webauthn_stage enables passkey autofill (conditional WebAuthn, + # authentik >= 2025.12): the browser offers enrolled passkeys in + # the username field's autofill — one tap, no username typed. + # Requires a discoverable (resident) credential; users whose + # credential is non-discoverable simply type their username and + # get the regular WebAuthn challenge at order 20 instead. + - id: passkey-passwordless-identification + model: authentik_stages_identification.identificationstage + identifiers: + name: passkey-passwordless-identification + state: present + attrs: + user_fields: + - username + - email + webauthn_stage: !KeyOf passkey-passwordless-webauthn-validation + - model: authentik_flows.flowstagebinding identifiers: target: !KeyOf passkey-passwordless-flow