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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user