mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test(auth): pin the step-up elevation boundary and publish its contract
Adds the structural guard that freezes the exact route set an elevation can unlock, plus the behavioural suite that drives each threat case against a real Postgres: a token alone mints nothing, an elevation minted for one token is refused for another (same account included), a second redemption fails, a concurrent redemption lets exactly one winner through, an expired one is refused, an elevation never reaches requireAdmin, a wrong password is throttled and audited without saying what was wrong, and the cookie flow is untouched. The OpenAPI registry carries both new routes and names the accepting set in prose, so the client implements against the contract rather than against a description of it.
This commit is contained in:
1 parent
e557d9e9e6
commit
433d983e60
4 files changed
+1059
-24
No files matched your search
+220
-13
@@ -1601,13 +1601,81 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/step-up/options:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Begin a passkey re-proof for a step-up elevation (Bearer only)
|
||||
description: "Returns SimpleWebAuthn assertion options scoped to the calling account's primary passkeys, plus a
|
||||
challenge id to present at POST /api/auth/step-up. Bearer-only: a cookie session is refused, because a browser
|
||||
re-proves its factor at login and carries the result on its session row. Returns 409 when the account has no
|
||||
passkey registered — use the password arm of the mint instead."
|
||||
responses:
|
||||
"200":
|
||||
description: Assertion options issued.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StepUpOptionsEnvelope"
|
||||
"401": *a1
|
||||
"409":
|
||||
description: No passkey registered on this account.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/step-up:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Mint a single-use step-up elevation (Bearer only)
|
||||
description: >-
|
||||
Re-prove the account password or a primary passkey and receive an opaque elevation that authorises exactly ONE
|
||||
second-factor-management call.
|
||||
|
||||
|
||||
The elevation is the Bearer transport's equivalent of the web's fresh-second-factor session stamp. It is bound
|
||||
to the exact token that minted it (another token, including the same account's, cannot redeem it), single-use,
|
||||
and valid for five minutes — the same window the cookie path uses. Present it as `X-Step-Up: hle_…` alongside
|
||||
the normal `Authorization: Bearer` header.
|
||||
|
||||
|
||||
Presenting the token alone mints nothing: the body must carry a fresh factor proof. Every failure — wrong
|
||||
password, no password set on an SSO-provisioned account, an assertion for another account, a stale challenge —
|
||||
returns the same 401 with the same prose, and is audited server-side. Rate-limited per account (5 / 15 min) and
|
||||
per source address.
|
||||
|
||||
|
||||
Accepting routes (the complete set): GET /api/auth/me/mfa; POST /api/auth/me/mfa/totp/setup; POST
|
||||
/api/auth/me/mfa/totp/confirm; POST /api/auth/me/mfa/disable; POST /api/auth/me/mfa/recovery-codes/regenerate;
|
||||
POST /api/auth/me/mfa/webauthn/register/options; POST /api/auth/me/mfa/webauthn/register/verify; PATCH and
|
||||
DELETE /api/auth/me/mfa/webauthn/{id}. Nothing else accepts one — admin endpoints stay cookie-only.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StepUpMintRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Factor re-proved — elevation issued.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StepUpMintEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/auth/me/mfa/totp/setup:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Begin TOTP enrollment (cookie session only)
|
||||
summary: Begin TOTP enrollment (cookie session or step-up elevation)
|
||||
description: Generates and stores a pending (encrypted) TOTP secret and returns the otpauth URI + Base32 secret. MFA is
|
||||
not active until /confirm. Cookie-only — a Bearer token cannot enrol MFA.
|
||||
not active until /confirm. Accepts a cookie session, or a Bearer token presenting a single-use elevation from
|
||||
POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
responses:
|
||||
"200":
|
||||
description: Pending secret created.
|
||||
@@ -1628,9 +1696,10 @@ paths:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Confirm TOTP enrollment (cookie session only)
|
||||
summary: Confirm TOTP enrollment (cookie session or step-up elevation)
|
||||
description: Verifies a code against the pending secret, activates the factor, and returns the one-time recovery codes.
|
||||
Cookie-only.
|
||||
Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the
|
||||
`X-Step-Up` header; a Bearer token alone is still refused.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1664,8 +1733,9 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Disable the second factor (step-up gated)
|
||||
description: Requires a fresh second-factor step-up (cookie session) AND a current TOTP or recovery code. Clears the
|
||||
secret and deletes recovery codes. Bearer can never satisfy the step-up gate.
|
||||
description: Requires a fresh second-factor step-up AND a current TOTP or recovery code. Clears the secret and deletes
|
||||
recovery codes. Accepts a cookie session, or a Bearer token presenting a single-use elevation from POST
|
||||
/api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1687,8 +1757,9 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Regenerate recovery codes (step-up gated)
|
||||
description: Invalidates the entire prior recovery-code set and returns a fresh batch once. Step-up gated; Bearer can
|
||||
never satisfy the gate.
|
||||
description: Invalidates the entire prior recovery-code set and returns a fresh batch once. Step-up gated. Accepts a
|
||||
cookie session, or a Bearer token presenting a single-use elevation from POST /api/auth/step-up in the
|
||||
`X-Step-Up` header; a Bearer token alone is still refused.
|
||||
responses:
|
||||
"200":
|
||||
description: Fresh recovery codes issued.
|
||||
@@ -1703,9 +1774,10 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Second-factor status (cookie session only)
|
||||
summary: Second-factor status (cookie session or step-up elevation)
|
||||
description: Whether TOTP is active, how many recovery codes remain, and the registered WebAuthn security keys. Metadata
|
||||
only — no secret, code, or public key. Cookie-only.
|
||||
only — no secret, code, or public key. Accepts a cookie session, or a Bearer token presenting a single-use
|
||||
elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
responses:
|
||||
"200":
|
||||
description: Second-factor status.
|
||||
@@ -1721,7 +1793,9 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Begin registering a security key as a second factor
|
||||
description: Returns SimpleWebAuthn creation options + a challenge id. Cookie-only — a Bearer token cannot enrol MFA.
|
||||
description: Returns SimpleWebAuthn creation options + a challenge id. Accepts a cookie session, or a Bearer token
|
||||
presenting a single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is
|
||||
still refused.
|
||||
responses:
|
||||
"200":
|
||||
description: Registration options issued.
|
||||
@@ -1738,7 +1812,8 @@ paths:
|
||||
- Auth
|
||||
summary: Finish registering a security key as a second factor
|
||||
description: Verifies the attestation against the user-bound challenge and stores the credential in the second-factor
|
||||
store (separate from primary passkeys). Cookie-only.
|
||||
store (separate from primary passkeys). Accepts a cookie session, or a Bearer token presenting a single-use
|
||||
elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1766,6 +1841,9 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Rename a registered security key
|
||||
description: Rename a registered second-factor security key. Accepts a cookie session, or a Bearer token presenting a
|
||||
single-use elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still
|
||||
refused.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -1798,7 +1876,8 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Remove a registered security key (step-up gated)
|
||||
description: Requires a fresh second-factor step-up (cookie session). Bearer can never satisfy the gate.
|
||||
description: Requires a fresh second-factor step-up. Accepts a cookie session, or a Bearer token presenting a single-use
|
||||
elevation from POST /api/auth/step-up in the `X-Step-Up` header; a Bearer token alone is still refused.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -11393,6 +11472,65 @@ components:
|
||||
- code
|
||||
- codeVerifier
|
||||
description: Exchange a one-time native OIDC handoff code (+ its PKCE verifier) for the standard native token bundle.
|
||||
StepUpMintRequest:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
const: password
|
||||
password:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 512
|
||||
required:
|
||||
- method
|
||||
- password
|
||||
- type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
const: passkey
|
||||
challengeId:
|
||||
type: string
|
||||
minLength: 1
|
||||
credential:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
rawId:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
const: public-key
|
||||
response:
|
||||
type: object
|
||||
propertyNames:
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
authenticatorAttachment:
|
||||
type: string
|
||||
enum:
|
||||
- platform
|
||||
- cross-platform
|
||||
clientExtensionResults:
|
||||
type: object
|
||||
propertyNames:
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
required:
|
||||
- id
|
||||
- rawId
|
||||
- type
|
||||
- response
|
||||
description: SimpleWebAuthn ceremony response payload.
|
||||
required:
|
||||
- method
|
||||
- challengeId
|
||||
- credential
|
||||
description: Re-prove the account password or a primary passkey to mint a single-use, token-bound step-up elevation.
|
||||
type: object
|
||||
MfaDisableRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -15271,6 +15409,75 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
StepUpOptionsEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/StepUpOptionsResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
StepUpOptionsResponse:
|
||||
type: object
|
||||
properties:
|
||||
options:
|
||||
type: object
|
||||
propertyNames:
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
description: SimpleWebAuthn assertion options for the caller's passkeys.
|
||||
challengeId:
|
||||
type: string
|
||||
required:
|
||||
- options
|
||||
- challengeId
|
||||
additionalProperties: false
|
||||
StepUpMintEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/StepUpMintResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
StepUpMintResponse:
|
||||
type: object
|
||||
properties:
|
||||
elevation:
|
||||
type: string
|
||||
description: Opaque single-use elevation (`hle_<64hex>`). Present it in the `X-Step-Up` header on ONE
|
||||
second-factor-management call. Returned exactly once; store it in memory only.
|
||||
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)))$
|
||||
expiresInSeconds:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
required:
|
||||
- elevation
|
||||
- expiresAt
|
||||
- expiresInSeconds
|
||||
additionalProperties: false
|
||||
TotpSetupEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user