> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Changing your own password with the Rolter control plane API

> POST /api/v1/auth/password lets a signed-in account change its own local password: it proves the current one, is throttled like a sign-in, and ends the account's other sessions.

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

```bash
curl -X POST "$ROLTER_URL/api/v1/auth/password" \
  -H "Authorization: Bearer $ROLTER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "<the password you sign in with>", "new_password": "<a new one>"}'
```

| 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

```json
{ "sessions_revoked": 2 }
```

`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_changed` entry 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](/configuration/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:

```json
{ "user": { "id": "…", "email": "…" }, "has_local_password": false }
```

`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.
