added passkey support (closes #6)
All checks were successful
test-and-lint / test-and-lint (pull_request) Successful in 2m50s
All checks were successful
test-and-lint / test-and-lint (pull_request) Successful in 2m50s
This commit is contained in:
@@ -6,13 +6,16 @@ MiauInv uses SQLite for persistent storage. The schema is initialized in `storag
|
||||
PRAGMA foreign_keys = ON;
|
||||
```
|
||||
|
||||
The database stores users, refresh tokens, 2FA recovery codes, inventory items, locations, projects, stock mappings, and project allocations.
|
||||
The database stores users, refresh tokens, 2FA recovery codes, passkey credentials, passkey challenge state, inventory items, locations, projects, stock mappings, and project allocations.
|
||||
|
||||
## Entity Overview
|
||||
|
||||
```text
|
||||
[users] 1 ──── N [refresh_tokens]
|
||||
[users] 1 ──── N [two_factor_recovery_codes]
|
||||
[users] 1 ──── N [passkey_credentials]
|
||||
|
||||
[passkey_challenges] stores short-lived WebAuthn ceremony state
|
||||
|
||||
[items] 1 ──── N [stock] N ──── 1 [locations]
|
||||
[items] 1 ──── N [project_items] N ──── 1 [projects]
|
||||
@@ -65,6 +68,34 @@ Stores recovery-code hashes for 2FA fallback login.
|
||||
|
||||
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.
|
||||
|
||||
### `passkey_credentials`
|
||||
|
||||
Stores WebAuthn passkey credentials for account login. Private keys are not stored by MiauInv; they remain in the authenticator, platform passkey provider, browser, or security key.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | Passkey row UUID. |
|
||||
| `user_id` | `TEXT` | Not null, foreign key to `users(id)` with `ON DELETE CASCADE` | Owning user. |
|
||||
| `credential_id` | `TEXT` | Not null, unique | Base64url-encoded WebAuthn credential ID. |
|
||||
| `name` | `TEXT` | Not null | User-visible passkey name. |
|
||||
| `credential_data` | `TEXT` | Not null | Serialized WebAuthn credential data, including public-key material and authenticator metadata. |
|
||||
| `created_at` | `INTEGER` | Not null | Unix timestamp for creation. |
|
||||
| `last_used_at` | `INTEGER` | Nullable | Unix timestamp when the passkey was last used successfully. |
|
||||
|
||||
### `passkey_challenges`
|
||||
|
||||
Stores short-lived server-side WebAuthn session data for registration and login ceremonies. The browser receives only an opaque `session_token`.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `token` | `TEXT` | Primary key | Opaque challenge token returned to the client. |
|
||||
| `user_id` | `TEXT` | Not null, default `''` | Owning user for registration challenges. Empty for discoverable login challenges. |
|
||||
| `ceremony` | `TEXT` | Not null | Challenge type, for example `register` or `login`. |
|
||||
| `session_data` | `TEXT` | Not null | Serialized WebAuthn session data. |
|
||||
| `expires_at` | `INTEGER` | Not null | Unix timestamp after which the challenge is rejected. |
|
||||
|
||||
Challenge rows are consumed once during the finish step and expired rows are cleaned up opportunistically.
|
||||
|
||||
### `items`
|
||||
|
||||
Stores tracked inventory items.
|
||||
@@ -124,7 +155,7 @@ Maps item quantities to projects.
|
||||
|
||||
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.
|
||||
`two_factor_recovery_codes.user_id` and `passkey_credentials.user_id` use `ON DELETE CASCADE`, so deleting a user also deletes their recovery-code and passkey rows.
|
||||
|
||||
### Uniqueness
|
||||
|
||||
@@ -134,6 +165,7 @@ The following values are unique:
|
||||
- `locations.name`
|
||||
- `projects.name`
|
||||
- `two_factor_recovery_codes(user_id, code_hash)`
|
||||
- `passkey_credentials.credential_id`
|
||||
|
||||
### Current schema limitations
|
||||
|
||||
@@ -162,6 +194,36 @@ When the password is updated:
|
||||
3. Existing refresh tokens for that user are revoked.
|
||||
4. A new session is issued.
|
||||
|
||||
### Passkey registration
|
||||
|
||||
When a passkey is registered:
|
||||
|
||||
1. The current password is verified.
|
||||
2. A WebAuthn registration challenge is stored in `passkey_challenges`.
|
||||
3. The finish step consumes the challenge.
|
||||
4. The WebAuthn credential is verified.
|
||||
5. A row is inserted into `passkey_credentials`.
|
||||
6. Existing refresh tokens for the user are revoked.
|
||||
7. A new session is issued for the current browser.
|
||||
|
||||
### Passkey login
|
||||
|
||||
When passkey login completes:
|
||||
|
||||
1. A discoverable WebAuthn login challenge is consumed from `passkey_challenges`.
|
||||
2. The credential assertion is verified against the stored credential data.
|
||||
3. The stored credential data and `last_used_at` value are updated.
|
||||
4. A normal access/refresh session is issued.
|
||||
|
||||
### Passkey removal
|
||||
|
||||
When a passkey is removed or all passkeys are disabled:
|
||||
|
||||
1. The current password is verified.
|
||||
2. The matching passkey row, or all rows for the user, are deleted.
|
||||
3. Existing refresh tokens for the user are revoked.
|
||||
4. A new session is issued for the current browser.
|
||||
|
||||
### 2FA enable
|
||||
|
||||
When 2FA is enabled:
|
||||
|
||||
Reference in New Issue
Block a user