updated docs for new feature

This commit is contained in:
2026-06-10 01:10:23 +02:00
parent fabe5319ae
commit ae41b96fa4
3 changed files with 575 additions and 260 deletions

View File

@@ -1,51 +1,213 @@
# Authentication Architecture
MiauInv implements a stateless JSON Web Token (JWT) architecture combined with a persistent database-backed Refresh Token mechanism to provide high security alongside seamless session retention.
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.
## Token Lifetime and Properties
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.
| Token Type | Transport Vector | Storage Location | Lifetime | Purpose |
| --- | --- | --- | --- | --- |
| **Access Token** | HTTP-Only Cookie & Auth Header | Memory / Browser Cookies | 15 Minutes | Signed payload validating current session identity for immediate API interaction. |
| **Refresh Token** | Secure Cookie & JSON Payload | LocalStorage / Secure Cookies | 7 Days | Long-lived high-entropy string used to request a new token pair when the Access Token expires. |
## 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 Rotation and Flow
## Token Types
The application coordinates token validation through cooperative interactions between Go authentication middlewares and the frontend runtime environment.
| 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`. |
### 1. Normal Authenticated Requests
During standard interaction loops, the Go server intercepts requests via auth middleware. It checks the incoming context for validity in the following order:
1. `Authorization: Bearer <token>` request header.
2. `access_token` cookie values.
## Normal Login Flow Without 2FA
If a valid, unexpired Access Token is recovered, the middleware parses the claims (ID, username, role) and injects them into the request context before execution routes fire.
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.
### 2. Token Refresh Flow
When an Access Token expires mid-session, the following workflow occurs automatically:
1. The backend rejects an API call or routing intent with an HTTP state indicating token expiration.
2. The frontend execution scope identifies the expiration status and reads the `refresh_token` from storage assets.
3. The client submits a POST request containing the token payload to `/api/refresh`.
4. The backend verifies the signature, looks up the hash inside the `refresh_tokens` table, and verifies that `revoked == 0` and `expires_at > now`.
5. If the validation succeeds, a brand-new Access Token and a rotated Refresh Token pair are generated, saved to secure cookies/storage, and the user session continues without explicit re-authentication.
---
## Security Mitigations
### Loop Protection
To prevent broken, expired, or malformed credentials from triggering infinite network refresh loops (which degrade browser performance and strain backend lookup performance), the frontend utilizes an explicit safety lock.
## 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": "..."
}
```
Token Expired -> Check 'is_refreshing' flag -> True -> Clear Auth & Force Login
-> False -> Set flag 'true' -> Send Request
5. The frontend shows the second login step.
6. Client sends `POST /api/login/2fa` with:
```json
{
"two_factor_token": "...",
"code": "123456"
}
```
Before issuing an evaluation request to `/api/refresh`, the application checks a temporary session variable (`is_refreshing` within `sessionStorage`). If the flag is already set to `true`, the loop protection triggers a hard clearance routine via `clearAllAuth()`, drops all token storage records, and routes the user back to the primary login view safely.
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.
### Database Revocation
Refresh sessions can be killed immediately from the server side. When a user requests `/api/logout`, the backend switches the corresponding row state within the `refresh_tokens` database container to `revoked = 1`. Any subsequent rotation requests relying on that token family are automatically dropped, protecting against stolen credential replay attacks.
## 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 stores the secret in `users.two_factor_secret`, but does not enable 2FA yet.
4. Server returns:
```json
{
"secret": "BASE32SECRET",
"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.
## Recovery Codes
Recovery codes are generated when 2FA is enabled and when the user regenerates them.
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.
## 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.
## 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:
- Add rate limiting for login, 2FA, refresh, and recovery-code attempts.
- 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.