add rate limiting and 2fa hardening
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
83
docs/SECURITY.md
Normal 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.
|
||||
Reference in New Issue
Block a user