mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
A browser session on its own could start TOTP enrollment or register a security key. Combined with a pending secret counting as a factor, that let a stolen session give itself a second factor, register a passkey with it, and keep access after the owner changed the password. TOTP setup and security-key registration now pass on the cookie path only when the session signed in or completed a second factor in the last five minutes, or when the body carries a proof in the step-up shape (password, a confirmed authenticator code, or a passkey or security-key assertion). The settings cards ask for that proof when the server answers auth.reproof.required and retry the same call with it. The Bearer path is unchanged: its elevation already is the proof. Every route that checks a password or a factor as a re-proof now uses one verifier and one per-account budget of five attempts per fifteen minutes, shared with the step-up mint, and records each refused proof as auth.mfa.failed. Passkey enrollment and password change had neither the limit (passkey enrollment) nor the audit row (both). A structural test freezes the set of credential-verifying routes and requires a throttle and an audit call in each.
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
/**
|
|
* Request schemas for the step-up elevation surface. Single-sourced here so the
|
|
* runtime `safeParse` and the OpenAPI registry describe the identical wire
|
|
* contract.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
import { webauthnCredentialSchema } from "@/lib/validations/mfa";
|
|
|
|
/**
|
|
* Re-prove a factor to mint an elevation. Four accepted proofs, discriminated on
|
|
* `method`, and the choice is an AUTHORISATION input rather than a preference:
|
|
*
|
|
* - `password` — the account password. Reaches exactly what a plain cookie
|
|
* session reaches. Unavailable on an account provisioned through OIDC SSO
|
|
* that never set one.
|
|
* - `totp` — a current code from the enrolled authenticator.
|
|
* - `webauthn` — an assertion from a registered second-factor security key,
|
|
* begun at `/api/auth/step-up/options` with `method: "webauthn"`.
|
|
* - `passkey` — an assertion from a primary passkey, begun at the same
|
|
* endpoint with `method: "passkey"`.
|
|
*
|
|
* Only the last three satisfy the fresh-factor routes, mirroring the web, where
|
|
* a password login never stamps a session second-factor-verified.
|
|
*
|
|
* The password is length-bounded so a body cap is not the only thing standing
|
|
* between a caller and an unbounded Argon2id verify.
|
|
*/
|
|
export const stepUpMintSchema = z
|
|
.discriminatedUnion("method", [
|
|
z.object({
|
|
method: z.literal("password"),
|
|
password: z.string().min(1).max(512),
|
|
}),
|
|
z.object({
|
|
method: z.literal("totp"),
|
|
code: z
|
|
.string()
|
|
.trim()
|
|
.regex(/^\d{6}$/, "Enter the 6-digit code from your authenticator app"),
|
|
}),
|
|
z.object({
|
|
method: z.literal("webauthn"),
|
|
challengeId: z.string().min(1),
|
|
credential: webauthnCredentialSchema,
|
|
}),
|
|
z.object({
|
|
method: z.literal("passkey"),
|
|
challengeId: z.string().min(1),
|
|
credential: webauthnCredentialSchema,
|
|
}),
|
|
])
|
|
.meta({
|
|
id: "StepUpMintRequest",
|
|
description:
|
|
"Re-prove a factor to mint a single-use, token-bound step-up elevation. `password` reaches the same routes a plain cookie session reaches; `totp`, `webauthn`, and `passkey` additionally satisfy the fresh-factor routes (disable, recovery-code rotation, security-key removal), matching exactly the ceremonies for which the web stamps a session second-factor-verified.",
|
|
});
|
|
|
|
export type StepUpMintRequest = z.infer<typeof stepUpMintSchema>;
|
|
|
|
/** Which assertion ceremony to begin at `POST /api/auth/step-up/options`. */
|
|
export const stepUpOptionsSchema = z
|
|
.object({
|
|
method: z
|
|
.enum(["passkey", "webauthn"])
|
|
.describe(
|
|
"`passkey` begins an assertion against the account's primary passkeys; `webauthn` against its registered second-factor security keys.",
|
|
),
|
|
})
|
|
.meta({ id: "StepUpOptionsRequest" });
|