feat(password): add reset and change password flows
Build, Test & Push Auth Service / quality-check (push) Successful in 53s
Build, Test & Push Auth Service / docker (push) Successful in 1m30s

Forgot password with a single-use reset link (1 hour, ACTIVE users
only, same answer for every address), change password (current session
stays, others end), admin reset mail with audit row, 'password
changed' mail. Login and refresh wait for a running password change.
This commit is contained in:
lobo committed 2026-10-02 10:35:08 +02:00
1 parent 6e7fb1e066
commit 5a2552ddf6
37 files changed
+1256 -34

No files matched your search

+31 -1
View File
@@ -16,6 +16,10 @@ To Be Played is split into three repositories:
- Registration by email confirmation (a single-use link, valid 24 hours, on whose page the owner chooses the
password; unconfirmed requests are deleted after 7 days) and then admin approval, login with email and password (BCrypt). Emails are stored trimmed and in
lower case (V19, unique on `lower(email)`); a password has 8 characters and at most 72 bytes (BCrypt's limit).
- **Password reset**: "forgot password" mails a link (`/reset-password#<token>`, single use, valid 1 hour, only the
SHA-256 stored, a new mail replaces the old link). Only an ACTIVE user gets a working link; every other address gets
the same `204` and no mail. A reset ends all logins; "change password" (current password + new one) ends all logins
but the current one. A PLATFORM_ADMIN can send the same mail to an active user. Both send a "password changed" mail.
- Brute-force protection in memory: 5 failed logins per email or 20 per IP, and 10 failed or 3 taken registrations
per IP, block for 15 minutes with `429 TOO_MANY_ATTEMPTS` and `Retry-After`. Unknown emails cost the same BCrypt time as a
wrong password, and every failed login gets the same error.
@@ -85,7 +89,7 @@ To Be Played is split into three repositories:
the deploy get the page.
- **Mails** from the platform mailbox: "a new registration waits" to the admin (`APP_MAIL_ADMIN_TO`, with the link
to the admin page), and to the user "confirm your address" (`CONFIRM_EMAIL`, sent first; the admin mail follows the click), then "your account is active" or "your
registration was denied", later "your account is blocked" and "your account will be deleted from <date>". The registration sends the browser language (`de` for a German browser, else `en`), which also becomes
registration was denied", later "your account is blocked" and "your account will be deleted from <date>", and "reset your password" (`PASSWORD_RESET`, the link) and "your password was changed" (`PASSWORD_CHANGED`, after a reset or a change). The registration sends the browser language (`de` for a German browser, else `en`), which also becomes
the user's app language. Plain text only. See "Mail flow".
- **Broadcast mail**: a PLATFORM_ADMIN writes one mail with subject and text in English and German (plain text),
and sends it to all active users or to picked ones, each in their app language. Every active user gets it.
@@ -108,6 +112,9 @@ To Be Played is split into three repositories:
| `POST` | `/api/registration/confirm` (`{token, password}`; `204`; `410 CONFIRMATION_INVALID`) | public |
| `POST` | `/api/registration/resend` (`{email}`; always `204`) | public |
| `POST` | `/api/auth/login` | public, sets both cookies |
| `POST` | `/api/auth/password-reset` (`{email}`; always `204`; `429 TOO_MANY_ATTEMPTS`: 5 per IP and 15 min) | public |
| `POST` | `/api/auth/password-reset/confirm` (`{token, password}`; `204`; `410 RESET_INVALID`; `429`) | public |
| `POST` | `/api/auth/password` (`{currentPassword, newPassword}`; `204`; `403 INVALID_PASSWORD`; `429`; under `/api/auth` for the refresh cookie, which keeps the current login) | access token |
| `POST` | `/api/auth/refresh` | refresh cookie, replaces both cookies |
| `POST` | `/api/auth/logout` | refresh cookie, clears both cookies |
| `GET` | `/api/users/me` | access token |
@@ -121,6 +128,7 @@ To Be Played is split into three repositories:
| `PUT` | `/api/admin/users/{id}/blocked` (`{blocked}`; `409 SELF_ACTION`, `TARGET_IS_ADMIN`, `ALREADY_DELETED`, `TARGET_IS_PENDING`) | PLATFORM_ADMIN |
| `DELETE` | `/api/admin/users/{id}` (`409 SELF_ACTION`, `TARGET_IS_ADMIN`, `TARGET_IS_PENDING`, `TARGET_IS_UNCONFIRMED`) | PLATFORM_ADMIN |
| `DELETE` | `/api/admin/users/{id}/unconfirmed` (hard delete of an unconfirmed request; `204`; `404`; `409 NOT_UNCONFIRMED`, `SELF_ACTION`, `TARGET_IS_ADMIN`) | PLATFORM_ADMIN |
| `POST` | `/api/admin/users/{id}/password-reset` (`204`; `409 NOT_ACTIVE`, `SELF_ACTION`, `TARGET_IS_ADMIN`) | PLATFORM_ADMIN |
| `GET` | `/api/hints` (`{results}`: running hints not closed for good, newest first) | access token |
| `PUT` | `/api/hints/{id}/done` (`204`; `404 HINT_NOT_FOUND`) | access token |
| `GET` | `/api/admin/hints` (`{results}` with `status` PLANNED/ACTIVE/ENDED by the server clock and `doneCount`, newest first) | PLATFORM_ADMIN |
@@ -339,6 +347,28 @@ sequenceDiagram
end
```
### Password reset flow
The link is valid for 1 hour and works once: the reset is one `UPDATE ... WHERE password_reset_hash = ? AND expires > now
AND <active>` that also clears the hash. At most 3 reset mails per address and hour (over all IPs) and 5 requests per IP
per 15 minutes; failed confirms are limited to 10 per IP. Other logins keep their access token until it expires (up to
15 minutes); their refresh tokens end at once.
```mermaid
sequenceDiagram
participant U as User
participant A as Auth
participant D as Admin
U->>A: POST /api/auth/password-reset {email}
A->>A: active user: store token hash + expiry, one mail row
A-->>U: 204 (same for any address)
A-->>U: mail "reset your password" (link with token)
U->>A: POST /api/auth/password-reset/confirm {token, password}
A->>A: new password, hash cleared, all refresh tokens revoked
A-->>U: 204 and mail "your password was changed"
Note over D,A: POST /api/admin/users/{id}/password-reset sends the same mail (active users only) and writes the audit log
```
### Steam ID and block sync to Core
Core keeps its own copy of the Steam ID and of the block. After a verified Steam link (also for the account that