8.7 KiB
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
- Client sends
POST /api/loginwithusernameandpassword. - Server loads the user by username.
- Server verifies the bcrypt password hash.
- 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.
- The refresh token is hashed before being stored in the
refresh_tokenstable.
Login Flow With 2FA Enabled
- Client sends
POST /api/loginwithusernameandpassword. - Server verifies the password.
- If
two_factor_enabledis true, the server does not issue a full session. - Instead, the server returns:
{
"requires_2fa": true,
"two_factor_token": "..."
}
- The frontend shows the second login step.
- Client sends
POST /api/login/2fawith:
{
"two_factor_token": "...",
"code": "123456"
}
- The server validates the purpose token against the expected purpose
2fa_login. - The server loads the user from the purpose-token claim.
- The supplied code is checked as a TOTP code first.
- If the TOTP check fails, the supplied value is normalized and checked as a recovery code.
- If either check succeeds, the server issues the normal access/refresh token session.
- 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.
- Authenticated user calls
POST /api/2fa/setup. - Server creates a TOTP secret using issuer
MiauInvand the current username as the account name. - Server creates a short-lived setup token containing the not-yet-enabled TOTP secret.
- The secret is not written to
users.two_factor_secretduring setup. - Server returns:
{
"secret": "BASE32SECRET",
"setup_token": "...",
"otpauth_url": "otpauth://totp/...",
"qr_code": "data:image/png;base64,..."
}
- The frontend displays the QR code and the manual setup key.
- User scans the QR code or enters the secret manually into an authenticator app.
- User submits the setup token and a current TOTP code to
POST /api/2fa/enable. - Server validates the setup token and the TOTP code.
- Server stores the TOTP secret, enables 2FA, invalidates any previous recovery codes, and generates a fresh recovery-code set.
- Server revokes existing refresh sessions and issues a new current session.
- 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:
- User enters a recovery code in the same field as the TOTP code during the second login step.
- Server first attempts normal TOTP validation.
- If TOTP validation fails, server hashes the normalized recovery code.
- Server updates the matching unused row with
used_at = now. - If exactly one row was updated, the recovery-code login succeeds.
Recovery-Code Regeneration
POST /api/2fa/recovery-codes/regenerate requires:
{
"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:
{
"password": "current-password",
"code": "123456"
}
If the password and TOTP code are valid, the server:
- Sets
two_factor_enabled = 0. - Clears
two_factor_secret. - Deletes recovery codes for the user.
- Revokes refresh tokens for the user.
- 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:
{
"refresh_token": "..."
}
or from the refresh_token cookie.
The server:
- Hashes the supplied token.
- Looks it up in
refresh_tokens. - Rejects revoked or expired tokens.
- Marks the used refresh token as revoked.
- Issues a new access token and refresh token.
- Stores the new refresh token hash.
- 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:
Authorization: Bearer <token>header.access_tokencookie.
If no token is found:
/api/*routes receive401 Unauthorized.- non-API routes redirect to
/login.
If token validation fails:
/api/*routes receive401 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.