updated docs for new feature
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user