mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(auth): allow trusting a device to skip the second factor
Add an opt-in "remember this device" at the second-factor step. On opt-in a random token is minted, set in an httpOnly+Secure+SameSite=Strict cookie, and only its HMAC hash is stored in a new TrustedDevice table. A later sign-in from the same browser still requires the password (factor 1); a matching, unexpired trusted device lets the login skip factor 2 only. The token is never accepted for step-up — a trusted-device login does not stamp mfaVerifiedAt — so a stolen device cookie cannot reach a destructive action. Trusted devices are listed and revocable from the security surface, are revoked wholesale when the second factor is removed or the user signs out everywhere, and cascade away on account deletion. A recovery-code login never trusts the browser. Migration 0204 adds the table (and the instance-wide mfa_required policy column that the enforcement work reads).
This commit is contained in:
1 parent
45a49f3d6d
commit
135bb04f0d
27 files changed
+1277
-8
No files matched your search
@@ -1779,6 +1779,60 @@ paths:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/me/trusted-devices:
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
summary: List trusted devices
|
||||
description: v1.23 — the 'remember this device' list. A trusted device skips the second factor for 30 days (the password
|
||||
is still required). Returns only an IP-free device label + lifecycle timestamps, never the token.
|
||||
responses:
|
||||
"200":
|
||||
description: Trusted devices for the caller.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrustedDeviceListEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
delete:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Forget every trusted device
|
||||
description: v1.23 — revokes all trusted devices for the caller and clears the caller's own trusted-device cookie.
|
||||
responses:
|
||||
"200":
|
||||
description: All trusted devices revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrustedDeviceRevokeAllEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/me/trusted-devices/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Revoke a single trusted device
|
||||
description: v1.23 — revokes one trusted device by id, scoped to the authenticated user (a foreign id returns 404).
|
||||
responses:
|
||||
"200":
|
||||
description: Trusted device revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrustedDeviceRevokeEnvelope"
|
||||
"401": *a1
|
||||
"404":
|
||||
description: Trusted device not found or not owned by the caller.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/me/security-activity:
|
||||
get:
|
||||
tags:
|
||||
@@ -7883,6 +7937,10 @@ components:
|
||||
type: string
|
||||
minLength: 6
|
||||
maxLength: 32
|
||||
rememberDevice:
|
||||
description: Opt in to trusting this device for 30 days — subsequent logins skip the second factor (the password is
|
||||
still required).
|
||||
type: boolean
|
||||
required:
|
||||
- mfaTicket
|
||||
- code
|
||||
@@ -8006,6 +8064,10 @@ components:
|
||||
- type
|
||||
- response
|
||||
description: SimpleWebAuthn ceremony response payload.
|
||||
rememberDevice:
|
||||
description: Opt in to trusting this device for 30 days — subsequent logins skip the second factor (the password is
|
||||
still required).
|
||||
type: boolean
|
||||
required:
|
||||
- mfaTicket
|
||||
- challengeId
|
||||
@@ -11685,6 +11747,112 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
TrustedDeviceListEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/TrustedDeviceListResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
TrustedDeviceListResponse:
|
||||
type: object
|
||||
properties:
|
||||
devices:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
label:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: Coarse, IP-free device label (e.g. 'Firefox on macOS').
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
lastUsedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
isCurrent:
|
||||
type: boolean
|
||||
description: True for the device making this request.
|
||||
required:
|
||||
- id
|
||||
- label
|
||||
- createdAt
|
||||
- lastUsedAt
|
||||
- expiresAt
|
||||
- isCurrent
|
||||
additionalProperties: false
|
||||
required:
|
||||
- devices
|
||||
additionalProperties: false
|
||||
TrustedDeviceRevokeAllEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
revoked:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
required:
|
||||
- revoked
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
TrustedDeviceRevokeEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
revoked:
|
||||
type: boolean
|
||||
required:
|
||||
- revoked
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
SecurityActivityEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user