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.
|
||||
|
||||
219
docs/DATABASE.md
219
docs/DATABASE.md
@@ -1,97 +1,194 @@
|
||||
# Database Documentation
|
||||
|
||||
MiauInv utilizes an embedded SQLite database instance for persistent data storage. Foreign key constraints are strictly enforced at the database level.
|
||||
MiauInv uses SQLite for persistent storage. The schema is initialized in `storage.InitDB` and foreign-key enforcement is explicitly enabled with:
|
||||
|
||||
## Configuration
|
||||
To ensure data integrity, every database connection initialization explicitly executes the following command before handling queries:
|
||||
```sql
|
||||
PRAGMA foreign_keys = ON;
|
||||
```
|
||||
|
||||
---
|
||||
The database stores users, refresh tokens, 2FA recovery codes, inventory items, locations, projects, stock mappings, and project allocations.
|
||||
|
||||
## Schema Architecture
|
||||
## Entity Overview
|
||||
|
||||
### Entity-Relationship Summary
|
||||
```text
|
||||
[users] 1 ──── N [refresh_tokens]
|
||||
[users] 1 ──── N [two_factor_recovery_codes]
|
||||
|
||||
The database consists of primary entity tables (`users`, `items`, `locations`, `projects`) and relational junction tables (`stock`, `project_items`, `refresh_tokens`) designed to track stock distribution and access sessions.
|
||||
|
||||
```
|
||||
[users] <--- (1:N) ---> [refresh_tokens]
|
||||
[items] <--- (1:N) ---> [stock] <--- (N:1) ---> [locations]
|
||||
[items] <--- (1:N) ---> [project_items] <--- (N:1) ---> [projects]
|
||||
[items] 1 ──── N [stock] N ──── 1 [locations]
|
||||
[items] 1 ──── N [project_items] N ──── 1 [projects]
|
||||
```
|
||||
|
||||
---
|
||||
## Tables
|
||||
|
||||
## Table Definitions
|
||||
### `users`
|
||||
|
||||
### 1. users
|
||||
Stores account credentials, roles, and 2FA state.
|
||||
|
||||
Stores user credentials and operational roles within the system.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | User UUID. |
|
||||
| `username` | `TEXT` | Not null, unique | Lowercased account name. |
|
||||
| `password` | `TEXT` | Not null | bcrypt password hash. |
|
||||
| `role` | `TEXT` | Not null | User role, for example `user` or `admin`. |
|
||||
| `two_factor_enabled` | `INTEGER` | Not null, default `0` | Boolean flag for TOTP 2FA state. |
|
||||
| `two_factor_secret` | `TEXT` | Not null, default `''` | TOTP secret used to validate authenticator codes. Empty when 2FA is disabled. |
|
||||
|
||||
* **id (TEXT, PK):** Unique UUID
|
||||
* **username (TEXT, Unique):** Unique account identifier.
|
||||
* **password (TEXT):** Hashed user password.
|
||||
* **role (TEXT):** Access control flag (e.g., admin, user).
|
||||
Migration note: existing databases are migrated with `ALTER TABLE` statements for `two_factor_enabled` and `two_factor_secret` if those columns do not exist yet.
|
||||
|
||||
### 2. refresh_tokens
|
||||
### `refresh_tokens`
|
||||
|
||||
Tracks valid extended sessions linked to specific user accounts.
|
||||
Stores refresh-token sessions. Tokens are stored as hashes, not plaintext.
|
||||
|
||||
* **id (TEXT, PK):** Unique identifier.
|
||||
* **user_id (TEXT, FK):** References `users(id)`.
|
||||
* **token_hash (TEXT):** Cryptographic hash of the active refresh token.
|
||||
* **expires_at (INTEGER):** Unix timestamp indicating token expiration.
|
||||
* **created_at (INTEGER):** Unix timestamp indicating session creation.
|
||||
* **revoked (INTEGER):** Boolean flag (0 or 1) indicating if the session was manually invalidated.
|
||||
* **device_info (TEXT, Optional):** Client metadata for auditing.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | Refresh-token row UUID. |
|
||||
| `user_id` | `TEXT` | Not null, foreign key to `users(id)` | Owning user. |
|
||||
| `token_hash` | `TEXT` | Not null | Hash of the refresh token. |
|
||||
| `expires_at` | `INTEGER` | Not null | Unix timestamp for expiry. |
|
||||
| `created_at` | `INTEGER` | Not null | Unix timestamp for creation. |
|
||||
| `revoked` | `INTEGER` | Not null, default `0` | Boolean revocation flag. |
|
||||
| `device_info` | `TEXT` | Optional | User-Agent string recorded when the session is created. |
|
||||
|
||||
### 3. items
|
||||
Refresh-token rotation revokes the used refresh token and inserts a new row for the next token.
|
||||
|
||||
Represents individual tracked assets.
|
||||
### `two_factor_recovery_codes`
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT):** Asset designation.
|
||||
* **category (TEXT, Optional):** Grouping classification.
|
||||
* **description (TEXT, Optional):** Detailed asset context.
|
||||
* **total_quantity (INTEGER):** Absolute global stock baseline counter.
|
||||
Stores recovery-code hashes for 2FA fallback login.
|
||||
|
||||
### 4. locations
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | Recovery-code row UUID. |
|
||||
| `user_id` | `TEXT` | Not null, foreign key to `users(id)` with `ON DELETE CASCADE` | Owning user. |
|
||||
| `code_hash` | `TEXT` | Not null, unique with `user_id` | Hash of the normalized recovery code. |
|
||||
| `created_at` | `INTEGER` | Not null | Unix timestamp for creation. |
|
||||
| `used_at` | `INTEGER` | Nullable | Unix timestamp when the code was consumed. `NULL` means unused. |
|
||||
|
||||
Defines logical or physical facilities.
|
||||
Recovery codes are deleted and replaced when the user regenerates them. A recovery code is consumed with an atomic update that only matches unused codes.
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT, Unique):** Unique facility naming constraint.
|
||||
### `items`
|
||||
|
||||
### 5. projects
|
||||
Stores tracked inventory items.
|
||||
|
||||
Defines distinct tasks or allocation targets.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Item ID. |
|
||||
| `name` | `TEXT` | Not null | Item name. |
|
||||
| `category` | `TEXT` | Optional | Category label. |
|
||||
| `description` | `TEXT` | Optional | Item description. |
|
||||
| `total_quantity` | `INTEGER` | Not null, default `0` | Global quantity baseline. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT, Unique):** Unique operational tracking name.
|
||||
* **description (TEXT, Optional):** Scope description.
|
||||
### `locations`
|
||||
|
||||
### 6. stock
|
||||
Stores physical or logical storage locations.
|
||||
|
||||
Junction table mapping physical asset distributions across facilities.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Location ID. |
|
||||
| `name` | `TEXT` | Not null, unique | Location name. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||
* **location_id (INTEGER, FK):** References `locations(id)`.
|
||||
* **quantity (INTEGER):** Specific quantity present at this location node.
|
||||
### `projects`
|
||||
|
||||
### 7. project_items
|
||||
Stores project contexts for item allocation.
|
||||
|
||||
Junction table tracking asset assignments dedicated to specific ongoing project environments.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Project ID. |
|
||||
| `name` | `TEXT` | Not null, unique | Project name. |
|
||||
| `description` | `TEXT` | Optional | Project description. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||
* **project_id (INTEGER, FK):** References `projects(id)`.
|
||||
* **quantity (INTEGER):** Quantity allocated to this project context.
|
||||
---
|
||||
### `stock`
|
||||
|
||||
## Data Integrity Constraints
|
||||
Maps item quantities to locations.
|
||||
|
||||
* **Foreign Keys:** Because standard `ON DELETE` cascades are not defined explicitly in the schema rules, SQLite blocks parent deletion actions if dependent rows exist in `stock` or `project_items`. You must clear out stock allocations and project associations manually before deleting an item, location, or project.
|
||||
* **Uniqueness:** String uniqueness constraints protect against duplicate namespace registration on `users(username)`, `locations(name)`, and `projects(name)`.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Stock row ID. |
|
||||
| `item_id` | `INTEGER` | Not null, foreign key to `items(id)` | Item reference. |
|
||||
| `location_id` | `INTEGER` | Not null, foreign key to `locations(id)` | Location reference. |
|
||||
| `quantity` | `INTEGER` | Not null | Quantity at this location. |
|
||||
|
||||
### `project_items`
|
||||
|
||||
Maps item quantities to projects.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Association row ID. |
|
||||
| `item_id` | `INTEGER` | Not null, foreign key to `items(id)` | Item reference. |
|
||||
| `project_id` | `INTEGER` | Not null, foreign key to `projects(id)` | Project reference. |
|
||||
| `quantity` | `INTEGER` | Not null | Quantity allocated to the project. |
|
||||
|
||||
## Data Integrity
|
||||
|
||||
### Foreign keys
|
||||
|
||||
Foreign keys are enabled per connection. Because most inventory foreign keys do not define explicit cascade behavior, SQLite blocks deletion of referenced items, locations, or projects while dependent rows exist.
|
||||
|
||||
`two_factor_recovery_codes.user_id` uses `ON DELETE CASCADE`, so deleting a user also deletes their recovery-code rows.
|
||||
|
||||
### Uniqueness
|
||||
|
||||
The following values are unique:
|
||||
|
||||
- `users.username`
|
||||
- `locations.name`
|
||||
- `projects.name`
|
||||
- `two_factor_recovery_codes(user_id, code_hash)`
|
||||
|
||||
### Current schema limitations
|
||||
|
||||
The current schema does not yet enforce all business rules at the database level. In particular:
|
||||
|
||||
- `items.total_quantity` has no explicit `CHECK(total_quantity >= 0)` constraint.
|
||||
- `stock.quantity` has no explicit `CHECK(quantity >= 0)` constraint.
|
||||
- `project_items.quantity` has no explicit `CHECK(quantity >= 0)` constraint.
|
||||
- Duplicate stock rows for the same `(item_id, location_id)` pair are not prevented by a unique constraint.
|
||||
- Duplicate project allocations for the same `(item_id, project_id)` pair are not prevented by a unique constraint.
|
||||
|
||||
These constraints should be added in a future migration once the desired application behavior is finalized.
|
||||
|
||||
## Important Queries and Behaviors
|
||||
|
||||
### User lookup
|
||||
|
||||
Users can be loaded by lowercased username or ID. Username updates store the new username lowercased.
|
||||
|
||||
### Password update
|
||||
|
||||
When the password is updated:
|
||||
|
||||
1. The new password is bcrypt-hashed.
|
||||
2. The `users.password` field is updated.
|
||||
3. Existing refresh tokens for that user are revoked.
|
||||
4. A new session is issued.
|
||||
|
||||
### 2FA enable
|
||||
|
||||
When 2FA is enabled:
|
||||
|
||||
1. The TOTP secret must already exist from `/api/2fa/setup`.
|
||||
2. The supplied TOTP code is validated.
|
||||
3. Existing recovery codes are deleted.
|
||||
4. New recovery-code hashes are inserted.
|
||||
5. `users.two_factor_enabled` is set to `1`.
|
||||
|
||||
### 2FA disable
|
||||
|
||||
When 2FA is disabled:
|
||||
|
||||
1. `two_factor_enabled` is set to `0`.
|
||||
2. `two_factor_secret` is cleared.
|
||||
3. Recovery-code rows for the user are deleted.
|
||||
4. Refresh tokens for the user are revoked.
|
||||
|
||||
### Recovery-code use
|
||||
|
||||
A recovery code is consumed with an update equivalent to:
|
||||
|
||||
```sql
|
||||
UPDATE two_factor_recovery_codes
|
||||
SET used_at = ?
|
||||
WHERE user_id = ? AND code_hash = ? AND used_at IS NULL;
|
||||
```
|
||||
|
||||
The login succeeds only if exactly one row is updated.
|
||||
|
||||
Reference in New Issue
Block a user