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.

View File

@@ -31,7 +31,7 @@ Stores account credentials, roles, and 2FA state.
| `password` | `TEXT` | Not null | bcrypt password hash. |
| `role` | `TEXT` | Not null | User role, for example `user` or `admin`. |
| `two_factor_enabled` | `INTEGER` | Not null, default `0` | Boolean flag for TOTP 2FA state. |
| `two_factor_secret` | `TEXT` | Not null, default `''` | TOTP secret used to validate authenticator codes. Empty when 2FA is disabled. |
| `two_factor_secret` | `TEXT` | Not null, default `''` | TOTP secret used to validate authenticator codes. Empty when 2FA is disabled. During setup the secret is held in a short-lived signed setup token and is only stored after the first valid TOTP code. |
Migration note: existing databases are migrated with `ALTER TABLE` statements for `two_factor_enabled` and `two_factor_secret` if those columns do not exist yet.
@@ -166,11 +166,12 @@ When the password is updated:
When 2FA is enabled:
1. The TOTP secret must already exist from `/api/2fa/setup`.
2. The supplied TOTP code is validated.
1. The temporary setup token is validated.
2. The supplied TOTP code is validated against the setup secret.
3. Existing recovery codes are deleted.
4. New recovery-code hashes are inserted.
5. `users.two_factor_enabled` is set to `1`.
5. `users.two_factor_enabled` is set to `1` and `users.two_factor_secret` is set to the confirmed secret.
6. Existing refresh tokens are revoked and a new session is issued for the current browser.
### 2FA disable

83
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,83 @@
# Security Notes
This document summarizes the current security-relevant behavior of MiauInv. It is intended as implementation documentation, not as a guarantee that the application is production-ready for untrusted public deployments.
## Authentication
MiauInv uses signed JWT access tokens, database-backed refresh tokens, and optional TOTP-based two-factor authentication.
JWTs are signed with `JWT_SECRET`. They are not encrypted. Normal access tokens and purpose tokens should therefore contain only identity and authorization metadata.
The short-lived 2FA setup token is a narrow exception: it carries the not-yet-enabled TOTP secret until the first authenticator code is validated. This avoids storing the setup secret in the database before 2FA is confirmed.
## Passwords
Passwords are hashed with bcrypt. Password updates require the current password. New passwords longer than bcrypt's effective 72-byte limit are rejected.
When a password is changed:
1. The new password is bcrypt-hashed.
2. The stored password hash is updated.
3. Existing refresh tokens for the user are revoked.
4. A new session is issued for the current browser.
## Sessions
Access tokens expire after 15 minutes. Refresh tokens expire after 7 days.
Refresh tokens are stored only as hashes in the database. Refresh-token rotation revokes the used refresh token and inserts a new token hash.
Security-sensitive account changes revoke existing refresh-token sessions.
## Cookies
Authentication cookies are set as HTTP-only secure cookies using `SameSite=Lax`.
Because the cookies are marked `Secure`, local development should use HTTPS. If the application is placed behind a reverse proxy, the deployment should preserve HTTPS semantics between the user and the proxy.
## Two-Factor Authentication
TOTP 2FA is optional per account.
The setup flow returns a QR code, a manual setup key, and a short-lived setup token. The TOTP secret is stored only after the user submits a valid code from their authenticator app.
When 2FA is enabled:
1. Any previous recovery codes are deleted.
2. A new recovery-code set is generated.
3. The TOTP secret is stored.
4. Existing refresh sessions are revoked.
5. A new current session is issued.
When 2FA is disabled:
1. The TOTP secret is cleared.
2. Recovery codes are deleted.
3. Existing refresh sessions are revoked.
4. Authentication cookies are cleared.
## Recovery Codes
Recovery codes are generated with cryptographically secure randomness and stored only as hashes.
They are displayed only immediately after generation or regeneration. They cannot be recovered later because the plaintext values are not stored.
Recovery codes are single-use. During login, a submitted value is first checked as a TOTP code. If that fails, the value is normalized, hashed, and matched against unused recovery-code hashes.
The account settings UI warns the user when the remaining unused recovery-code count is low.
## Rate Limiting
Basic in-memory rate limiting protects login, 2FA, refresh, registration, and sensitive account endpoints.
This is suitable for a single-instance private deployment. It is not sufficient for multi-instance deployments because limiter state is process-local. A public or multi-instance deployment should use persistent or distributed rate limiting at the application, reverse proxy, or infrastructure layer.
## Known Limitations
- Automated testing is currently limited.
- TOTP secrets are stored in the database after confirmation because the server must validate future codes.
- TOTP secrets are not encrypted at rest.
- There is no dedicated session/device management UI yet.
- There is no audit log for account security changes yet.
- The current rate limiter is process-local and memory-only.
- Passkeys/WebAuthn are intentionally not implemented yet.