add rate limiting and 2fa hardening

This commit is contained in:
2026-06-10 01:35:36 +02:00
parent ae41b96fa4
commit 58f098d4ca
11 changed files with 410 additions and 59 deletions

View File

@@ -2,7 +2,7 @@
MiauInv uses signed JWT access tokens, database-backed refresh tokens, and optional TOTP-based two-factor authentication. The authentication flow is implemented in the `auth/`, `handlers/`, `storage/`, and `frontend/assets/js/` packages.
JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variable. They are not encrypted. Claims should therefore contain identity and authorization metadata only, not secrets.
JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variable. They are not encrypted. Access tokens and normal purpose tokens should therefore contain identity and authorization metadata only, not secrets. The short-lived 2FA setup token intentionally carries the not-yet-enabled TOTP secret because the same secret is already returned to the authenticated browser for QR/manual setup and is not stored server-side until confirmation.
## Components
@@ -22,6 +22,7 @@ JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variab
| Access token | `access_token` HTTP-only secure cookie and JSON response body | 15 minutes | Authenticates normal API and page requests. |
| Refresh token | `refresh_token` HTTP-only secure cookie and JSON response body | 7 days | Rotates sessions after access-token expiry. Stored in the database only as a hash. |
| 2FA challenge token | JSON response from `/api/login` | 5 minutes | Allows `/api/login/2fa` to complete login after password verification. It is purpose-bound to `2fa_login`. |
| 2FA setup token | JSON response from `/api/2fa/setup` | 10 minutes | Carries the not-yet-enabled TOTP secret until `/api/2fa/enable` validates the first code. It is purpose-bound to `2fa_setup`. |
## Normal Login Flow Without 2FA
@@ -72,27 +73,30 @@ The account settings page at `/profile/settings` exposes the UI for TOTP setup.
1. Authenticated user calls `POST /api/2fa/setup`.
2. Server creates a TOTP secret using issuer `MiauInv` and the current username as the account name.
3. Server stores the secret in `users.two_factor_secret`, but does not enable 2FA yet.
4. Server returns:
3. Server creates a short-lived setup token containing the not-yet-enabled TOTP secret.
4. The secret is not written to `users.two_factor_secret` during setup.
5. Server returns:
```json
{
"secret": "BASE32SECRET",
"setup_token": "...",
"otpauth_url": "otpauth://totp/...",
"qr_code": "data:image/png;base64,..."
}
```
5. The frontend displays the QR code and the manual setup key.
6. User scans the QR code or enters the secret manually into an authenticator app.
7. User submits a current TOTP code to `POST /api/2fa/enable`.
8. Server validates the code.
9. Server enables 2FA and generates recovery codes.
10. Recovery codes are returned once to the client for download/copying.
6. The frontend displays the QR code and the manual setup key.
7. User scans the QR code or enters the secret manually into an authenticator app.
8. User submits the setup token and a current TOTP code to `POST /api/2fa/enable`.
9. Server validates the setup token and the TOTP code.
10. Server stores the TOTP secret, enables 2FA, invalidates any previous recovery codes, and generates a fresh recovery-code set.
11. Server revokes existing refresh sessions and issues a new current session.
12. Recovery codes are returned once to the client for download/copying.
## Recovery Codes
Recovery codes are generated when 2FA is enabled and when the user regenerates them.
Recovery codes are generated when 2FA is enabled and when the user regenerates them. Enabling 2FA deletes any old recovery-code rows before inserting the new set.
Properties:
@@ -123,7 +127,7 @@ Recovery-code login flow:
}
```
The server verifies the current password and a current TOTP code. If both are valid, all previous recovery codes are deleted and a new set is inserted. The new plaintext codes are returned once.
The server verifies the current password and a current TOTP code. If both are valid, all previous recovery codes are deleted and a new set is inserted. The new plaintext codes are returned once. The account settings UI warns the user when the number of unused recovery codes is low.
## Disabling 2FA
@@ -183,6 +187,8 @@ Username changes require the current password.
Password changes require the current password, reject passwords longer than bcrypt's 72-byte effective limit, revoke existing refresh tokens, and issue a new session.
2FA activation also revokes existing refresh tokens and issues a new session for the current browser. Other devices must log in again and complete 2FA.
## Middleware Behavior
Protected routes use `AuthMiddleware`.
@@ -206,7 +212,7 @@ If token validation fails:
The current implementation is usable for private/self-hosted deployments, but these improvements should be prioritized before exposing it to untrusted public traffic:
- Add rate limiting for login, 2FA, refresh, and recovery-code attempts.
- Replace the current in-memory rate limiter with persistent or distributed rate limiting if the app is deployed across multiple instances.
- Add audit logging for account security changes.
- Add optional session/device management UI.
- Consider encrypting TOTP secrets at rest if the deployment threat model includes database disclosure.