A signed-in account changes its own password with POST /api/v1/auth/password.
The route writes only the caller’s own row, so it needs no role: a viewer, a
member and an account with no membership anywhere can all use it. It does not
widen user:update, the capability that resets anyone’s password
(PUT /api/v1/users/{id}) and stays superadmin-only. GET /api/v1/rbac/matrix
publishes it as the my_password resource, open to every authenticated caller.
In the dashboard#
The same change is the Password panel on Settings → Account & keys, between your profile and two-factor authentication. Every signed-in account sees it, whatever its role:
- It asks for the current password, the new password (at least 8 characters, said under the field) and the new password again. A password that is too short, the same as the current one, or not repeated identically is refused on its field before anything is sent, so it costs no failed attempt.
- A wrong current password is shown on the Current password field. It counts against the same limit as a wrong password at sign-in, so after too many the panel says how many seconds to wait, taken from
Retry-After; while the lock holds, the right password is refused as well. - On success the form empties and the panel says how many of your other sessions were signed out. The session you are in stays signed in.
- An account that signs in through single sign-on only has no local password, and the panel says so, and where to change it, in place of the form. An open-mode control plane has no accounts, so the panel is not drawn.
Request#
| Field | Type | Meaning |
|---|---|---|
current_password | string | the password the account signs in with today, proved again before anything changes |
new_password | string | at least 8 characters, the same minimum as every other way of setting a password |
Both fields are required and no others are accepted. The bearer must be a session
token; the admin token is not an account and has no password to change, and a
control plane running in open mode has no accounts at all (401
open_mode_no_session).
Response#
sessions_revoked is how many of the account’s other sessions the change ended.
The session the request came from stays signed in, so the person changing their
password is not signed out of the page they did it from.
What the change does#
- The old password stops working at once, and the new one signs in.
- Every other session of the account ends. The usual reason to change a password is that the old one is in someone else’s hands, and whoever signed in with it would otherwise keep a session for its full seven days.
- A sign-in that was halfway through its second-factor step with the old password is cancelled too. A second factor, if the account has one, is left as it is.
- An
auth.password_changedentry is written to the audit log with the number of sessions ended. No password, current or new, ever reaches the log.
Wrong passwords are throttled like sign-ins#
A wrong current_password is counted exactly like a failed sign-in, on the same
per-account and per-address counters (see
Environment variables for
ROLTER_LOGIN_MAX_FAILURES and the rest). A stolen session therefore cannot be
used to guess the password it was meant not to reveal, and the guesses made here
and at the sign-in screen draw on one budget. Each miss waits a growing delay
before it answers, and the miss that spends the budget locks the account for a
while; while it is locked the right password is refused as well, with 429 and a
Retry-After header saying how many seconds are left. A correct password clears
the count, as a successful sign-in does.
Each rejected guess is written to the audit log as auth.password_change_failed;
the one that engages a lock carries locked_for_secs.
Errors#
Every error is {"error": {"message": "...", "code": "..."}}; a refusal about one
field also carries that field’s name in error.field.
| Status | error.code | error.field | Meaning |
|---|---|---|---|
| 400 | invalid_field | current_password | the current password is not correct. It is a 400 and not a 401, so a client does not take it for a dead session |
| 400 | invalid_field | new_password | shorter than 8 characters, or the same as the current password |
| 400 | none | none | the body is malformed, is missing a field, or carries a field the route does not take |
| 401 | unauthenticated | none | no session, or the session has ended |
| 401 | open_mode_no_session | none | the control plane is in open mode and has no accounts |
| 409 | no_local_password | none | the account signs in through single sign-on only and has no local password to change |
| 429 | too_many_attempts | none | the account or the client address has spent its failed-attempt budget; wait Retry-After seconds |
A length problem or an account without a password is refused before anything is guessed, so neither costs the account a failure or runs a password verification. The new password is also compared with the current one only after the current one is proved, so that refusal tells someone who does not know it nothing.
Accounts that sign in through single sign-on#
An account created by single sign-on has no password until a superadmin sets one
under Governance → Users (PUT /api/v1/users/{id} with a password), so there
is nothing here to change and the route answers 409 no_local_password. To avoid
offering a form that cannot work, GET /api/v1/auth/me reports it up front:
has_local_password is true for every account that has a password, whether or
not its organization currently allows password sign-in. The hash itself never
leaves the server.