From 650dcba0eaaf6a7cb6664a0c31eaaba249c64332 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 24 Sep 2026 21:28:14 -0700 Subject: [PATCH] docs(authentik): password change is a prompt field, not a PasswordStage The UserWriteStage writes prompt_data['password'] via user.set_password(). A PasswordStage only authenticates the current password. Document the correct mechanism (password field on the default-user-settings prompt + empty-password guard policy) and the throwaway-user verification. --- .../Infrastructure/Authentik.md | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/serverAppConfigs-docs/Infrastructure/Authentik.md b/serverAppConfigs-docs/Infrastructure/Authentik.md index d67a377..92182d6 100644 --- a/serverAppConfigs-docs/Infrastructure/Authentik.md +++ b/serverAppConfigs-docs/Infrastructure/Authentik.md @@ -285,20 +285,33 @@ insertion order): | order | stage | type | what it does | |---|---|---|---| -| 20 | `default-user-settings` | `PromptStage` | intro prompt | -| 100 | `default-user-settings-write` | `UserWriteStage` | edit name / email / locale | -| 101 | `default-user-settings-password` | `PasswordStage` | change password (`InbuiltBackend`) | +| 20 | `default-user-settings` | `PromptStage` | name / email / locale **+ password** | +| 100 | `default-user-settings-write` | `UserWriteStage` | writes the prompt data to the user | -**The `PasswordStage` was missing until 2026-09-25.** The stock flow only -carried the prompt + `UserWriteStage`, so users could edit name/email/locale -but had no way to change their password. The fix: create a `PasswordStage` -(backend `authentik.core.auth.InbuiltBackend`) and bind it to the flow. Two -ORM gotchas: `Flow.stages.add()` fails on this through model +**Password change is a prompt field, not a stage.** The `UserWriteStage` +writes whatever the preceding `PromptStage` collected: for the key `password` +it calls `user.set_password(value)` (`stages/user_write/stage.py`). So the fix +is a `password` field on the `default-user-settings` prompt +(`field_key=password`, `type=password`, `required=False`, order 204) — NOT a +`PasswordStage`. A `PasswordStage` is an *authentication* stage: its +`validate_password` runs `authenticate()` against the backends to check the +*current* password; it never sets a new one. (A `PasswordStage` was added on +2026-09-25 and removed the same day for exactly this reason.) + +Two ORM gotchas: `Flow.stages.add()` fails on this through model (`Can't bulk create a multi-table inherited model` — `FlowStageBinding` is MTI), so create the `FlowStageBinding` row directly with an explicit `order`; -and `PasswordStage` requires the `backends` array (a bare `name` insert hits a -`NOT NULL` violation). Verified via the `FlowPlanner` — the plan now lists all -three stages. +and a prompt field is a `Prompt` row linked through `PromptStageField` +(`prompt_stage` + `prompt`; the order comes from `Prompt.order`). + +**A blank password must not set an empty password.** `set_password("")` would +make the account log in with no password, so a validation policy +(`default-user-settings-no-empty-password`, bound to the prompt via +`PromptStageValidationPolicy`) denies the form when `prompt_data["password"]` +is `""` and allows it when set. Verified with the `PolicyEvaluator` +(empty → deny, set → allow, absent → allow) and end-to-end by driving +`UserWriteStageView.update_user` on a throwaway user (new password valid, old +invalid). ## Outpost claim cache (`proxysession`) — 403 after a group add