Files
MiauInv/docs/AUTHENTICATION.md

220 lines
8.7 KiB
Markdown

# Authentication Architecture
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. 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
| Component | Location | Responsibility |
| --- | --- | --- |
| JWT helpers | `auth/jwt.go` | Access-token and purpose-token generation/validation. |
| Middleware | `auth/middleware.go` | Extracts access tokens from bearer headers or cookies and injects claims into the request context. |
| Password helpers | `auth/password.go` | bcrypt hashing and verification. |
| Login/account handlers | `handlers/account.go` | Register, login, 2FA, refresh, logout, account settings, and user metadata. |
| Persistent session storage | `storage/storage.go` | Refresh tokens, 2FA state, TOTP secret, and recovery-code hashes. |
| Frontend auth logic | `frontend/assets/js/auth.js`, `frontend/assets/js/login.js`, `frontend/assets/js/api.js` | Login UI, token refresh, account settings, and 2FA UI interactions. |
## Token Types
| Token | Storage/Transport | Lifetime | Purpose |
| --- | --- | --- | --- |
| 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
1. Client sends `POST /api/login` with `username` and `password`.
2. Server loads the user by username.
3. Server verifies the bcrypt password hash.
4. If 2FA is disabled, the server issues:
- a signed access token,
- a high-entropy refresh token,
- HTTP-only secure cookies for both tokens,
- a JSON response containing the same token values and user metadata.
5. The refresh token is hashed before being stored in the `refresh_tokens` table.
## Login Flow With 2FA Enabled
1. Client sends `POST /api/login` with `username` and `password`.
2. Server verifies the password.
3. If `two_factor_enabled` is true, the server does not issue a full session.
4. Instead, the server returns:
```json
{
"requires_2fa": true,
"two_factor_token": "..."
}
```
5. The frontend shows the second login step.
6. Client sends `POST /api/login/2fa` with:
```json
{
"two_factor_token": "...",
"code": "123456"
}
```
7. The server validates the purpose token against the expected purpose `2fa_login`.
8. The server loads the user from the purpose-token claim.
9. The supplied code is checked as a TOTP code first.
10. If the TOTP check fails, the supplied value is normalized and checked as a recovery code.
11. If either check succeeds, the server issues the normal access/refresh token session.
12. If a recovery code was used, it is marked as used and cannot be used again.
## TOTP Setup Flow
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 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,..."
}
```
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. Enabling 2FA deletes any old recovery-code rows before inserting the new set.
Properties:
- Generated with cryptographically secure randomness.
- Formatted as four groups of five hexadecimal characters.
- Normalized before hashing by removing spaces and hyphens and lowercasing the value.
- Stored only as hashes in `two_factor_recovery_codes`.
- Single-use only.
- Displayed only immediately after generation/regeneration.
- Downloaded client-side from the account settings page as `miauinv-recovery-codes.txt`.
Recovery-code login flow:
1. User enters a recovery code in the same field as the TOTP code during the second login step.
2. Server first attempts normal TOTP validation.
3. If TOTP validation fails, server hashes the normalized recovery code.
4. Server updates the matching unused row with `used_at = now`.
5. If exactly one row was updated, the recovery-code login succeeds.
## Recovery-Code Regeneration
`POST /api/2fa/recovery-codes/regenerate` requires:
```json
{
"password": "current-password",
"code": "123456"
}
```
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
`POST /api/2fa/disable` requires:
```json
{
"password": "current-password",
"code": "123456"
}
```
If the password and TOTP code are valid, the server:
1. Sets `two_factor_enabled = 0`.
2. Clears `two_factor_secret`.
3. Deletes recovery codes for the user.
4. Revokes refresh tokens for the user.
5. Clears authentication cookies.
The frontend redirects the user to `/login` after disabling 2FA because existing sessions are revoked.
## Refresh Token Rotation
`POST /api/refresh` accepts the refresh token either from JSON:
```json
{
"refresh_token": "..."
}
```
or from the `refresh_token` cookie.
The server:
1. Hashes the supplied token.
2. Looks it up in `refresh_tokens`.
3. Rejects revoked or expired tokens.
4. Marks the used refresh token as revoked.
5. Issues a new access token and refresh token.
6. Stores the new refresh token hash.
7. Updates the auth cookies.
## Account Settings Security
The account settings page currently supports:
- username changes,
- password changes,
- TOTP 2FA setup,
- TOTP 2FA disable,
- recovery-code download after generation,
- recovery-code regeneration.
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`.
The middleware checks tokens in this order:
1. `Authorization: Bearer <token>` header.
2. `access_token` cookie.
If no token is found:
- `/api/*` routes receive `401 Unauthorized`.
- non-API routes redirect to `/login`.
If token validation fails:
- `/api/*` routes receive `401 Unauthorized`.
- non-API routes clear the access cookie and redirect to `/login`.
## Security Limitations and Follow-up Work
The current implementation is usable for private/self-hosted deployments, but these improvements should be prioritized before exposing it to untrusted public traffic:
- 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.
- Expand tests for all authentication and account settings handlers.