Files
MiauInv/docs/AUTHENTICATION.md

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

  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:
{
  "requires_2fa": true,
  "two_factor_token": "..."
}
  1. The frontend shows the second login step.
  2. Client sends POST /api/login/2fa with:
{
  "two_factor_token": "...",
  "code": "123456"
}
  1. The server validates the purpose token against the expected purpose 2fa_login.
  2. The server loads the user from the purpose-token claim.
  3. The supplied code is checked as a TOTP code first.
  4. If the TOTP check fails, the supplied value is normalized and checked as a recovery code.
  5. If either check succeeds, the server issues the normal access/refresh token session.
  6. 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:
{
  "secret": "BASE32SECRET",
  "setup_token": "...",
  "otpauth_url": "otpauth://totp/...",
  "qr_code": "data:image/png;base64,..."
}
  1. The frontend displays the QR code and the manual setup key.
  2. User scans the QR code or enters the secret manually into an authenticator app.
  3. User submits the setup token and a current TOTP code to POST /api/2fa/enable.
  4. Server validates the setup token and the TOTP code.
  5. Server stores the TOTP secret, enables 2FA, invalidates any previous recovery codes, and generates a fresh recovery-code set.
  6. Server revokes existing refresh sessions and issues a new current session.
  7. 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:

{
  "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:

  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:

{
  "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.