added passkey support (closes #6)
All checks were successful
test-and-lint / test-and-lint (pull_request) Successful in 2m50s

This commit is contained in:
2026-06-10 03:24:31 +02:00
parent 01ec41288a
commit fb3be56959
18 changed files with 1680 additions and 61 deletions

View File

@@ -1,6 +1,6 @@
# 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.
MiauInv uses signed JWT access tokens, database-backed refresh tokens, optional TOTP-based two-factor authentication, and optional WebAuthn passkey 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.
@@ -12,8 +12,9 @@ JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variab
| 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. |
| Passkey handlers | `handlers/passkeys.go` | WebAuthn/passkey registration, login, removal, and disable flows. |
| Persistent session storage | `storage/storage.go`, `storage/passkeys.go` | Refresh tokens, 2FA state, TOTP secret, recovery-code hashes, passkey credentials, and WebAuthn challenge state. |
| Frontend auth logic | `frontend/assets/js/auth.js`, `frontend/assets/js/login.js`, `frontend/assets/js/api.js` | Login UI, passkey login, token refresh, account settings, 2FA UI interactions, and passkey UI interactions. |
## Token Types
@@ -23,6 +24,7 @@ JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variab
| 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`. |
| Passkey ceremony token | JSON response from `/api/passkeys/*/options` | 5 minutes | Opaque server-side reference to WebAuthn session data stored in `passkey_challenges`. Used for passkey registration and login. |
## Normal Login Flow Without 2FA
@@ -67,6 +69,47 @@ JWTs are signed with a symmetric secret from the `JWT_SECRET` environment variab
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.
## Passkey Registration Flow
Passkeys are managed from `/profile/settings`. Registration requires an authenticated session and current-password confirmation.
1. Client sends `POST /api/passkeys/register/options` with a passkey name and the current password.
2. Server verifies the password.
3. Server creates WebAuthn registration options with resident-key and user-verification requirements.
4. Server stores the WebAuthn session data in `passkey_challenges` and returns only an opaque `session_token` plus the public registration options.
5. Browser calls `navigator.credentials.create()` with the returned public-key options.
6. Client sends the browser credential response to `POST /api/passkeys/register/finish`.
7. Server consumes the one-time challenge, verifies the WebAuthn response, stores the credential, revokes existing refresh sessions, and issues a new current session.
The server stores credential metadata and public-key credential data. It does not store private keys.
## Passkey Login Flow
Passkey login is available from the normal login page.
1. Client sends `POST /api/passkeys/login/options`.
2. Server creates a discoverable passkey login challenge without requiring a username.
3. Server stores the WebAuthn session data in `passkey_challenges` and returns an opaque `session_token` plus assertion options.
4. Browser calls `navigator.credentials.get()` with the returned public-key options.
5. Client sends the browser assertion response to `POST /api/passkeys/login/finish`.
6. Server consumes the one-time challenge and verifies the WebAuthn assertion.
7. The stored credential data is updated after successful login.
8. Server issues a normal access/refresh session.
Passkey login is treated as a complete phishing-resistant sign-in method. The application requires WebAuthn user verification for passkey registration and login, so a valid passkey assertion is not followed by a separate TOTP challenge.
## Passkey Management
The account settings page supports:
- listing registered passkeys,
- adding a passkey,
- removing a single passkey,
- disabling all passkeys.
Adding, removing, or disabling passkeys revokes existing refresh sessions and issues a fresh session for the current browser. Removing or disabling passkeys requires current-password confirmation.
## TOTP Setup Flow
The account settings page at `/profile/settings` exposes the UI for TOTP setup.
@@ -178,6 +221,9 @@ The account settings page currently supports:
- username changes,
- password changes,
- passkey registration,
- passkey removal,
- passkey disable,
- TOTP 2FA setup,
- TOTP 2FA disable,
- recovery-code download after generation,
@@ -187,7 +233,7 @@ 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.
2FA activation also revokes existing refresh tokens and issues a new session for the current browser. Passkey changes also revoke existing refresh tokens and issue a new current session. Other devices must log in again and complete the configured authentication flow.
## Middleware Behavior
@@ -215,5 +261,5 @@ The current implementation is usable for private/self-hosted deployments, but th
- 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.
- Consider encrypting TOTP secrets and passkey credential data at rest if the deployment threat model includes database disclosure.
- Expand tests for all authentication and account settings handlers.