mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
chore(deps): move to SimpleWebAuthn 14 and React 19.3
Bump @simplewebauthn/server 13.3.3 -> 14.0.2 and @simplewebauthn/browser 13.3.0 -> 14.0.0, and keep React on an exact pin at 19.3.0 with react-dom and both type packages moved alongside. SimpleWebAuthn 14 changes one default the server relied on: the verifier used to accept every algorithm the library knew (EdDSA, ES256, ES512, PS256/384/512, RS256/384/512, RS1) and now accepts only the three it offers. Pass both lists explicitly so a registration that succeeded before still succeeds, and so the offered list stays EdDSA/ES256/RS256 on a runtime that would otherwise prepend ML-DSA-44. The option JSON of all four ceremonies, and verification against stored credentials, match the 13.x snapshots unchanged.
This commit is contained in:
1 parent
831fc9e0a7
commit
76ca6bbb30
6 files changed
+950
-824
No files matched your search
+6
-6
@@ -65,8 +65,8 @@
|
||||
"@parse/node-apn": "^8.1.0",
|
||||
"@prisma/adapter-pg": "^7.10.0",
|
||||
"@prisma/client": "^7.10.0",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@simplewebauthn/server": "^13.3.3",
|
||||
"@simplewebauthn/browser": "^14.0.0",
|
||||
"@simplewebauthn/server": "^14.0.2",
|
||||
"@tanstack/react-query": "5.102.8",
|
||||
"@tanstack/react-virtual": "^3.14.12",
|
||||
"@types/sax": "^1.2.7",
|
||||
@@ -90,9 +90,9 @@
|
||||
"prisma": "^7.10.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "19.2.7",
|
||||
"react": "19.3.0",
|
||||
"react-day-picker": "^10.0.1",
|
||||
"react-dom": "19.2.7",
|
||||
"react-dom": "19.3.0",
|
||||
"react-hook-form": "7.83.0",
|
||||
"recharts": "^3.10.1",
|
||||
"rrule": "^2.8.1",
|
||||
@@ -116,8 +116,8 @@
|
||||
"@types/node": "26.5.1",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
|
||||
Generated
+837
-818
File diff suppressed because it is too large.
Load diff
@@ -42,6 +42,10 @@ import {
|
||||
} from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { isoCBOR } from "@simplewebauthn/server/helpers";
|
||||
import {
|
||||
generateRegistrationOptions,
|
||||
verifyRegistrationResponse,
|
||||
} from "@simplewebauthn/server";
|
||||
|
||||
// ── In-memory stand-in for the four tables the ceremonies touch ──────
|
||||
|
||||
@@ -110,6 +114,16 @@ vi.mock("@/lib/db", () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Pass-through spies: the real library runs, the test can read the arguments.
|
||||
vi.mock("@simplewebauthn/server", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@simplewebauthn/server")>();
|
||||
return {
|
||||
...real,
|
||||
generateRegistrationOptions: vi.fn(real.generateRegistrationOptions),
|
||||
verifyRegistrationResponse: vi.fn(real.verifyRegistrationResponse),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkAuthSurfaceRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
@@ -130,6 +144,7 @@ import {
|
||||
createMfaAuthenticationOptions,
|
||||
createMfaRegistrationOptions,
|
||||
verifyMfaAuthentication,
|
||||
verifyMfaRegistration,
|
||||
} from "../mfa/webauthn";
|
||||
import { POST as LOGIN_OPTIONS } from "@/app/api/auth/passkey/login-options/route";
|
||||
|
||||
@@ -655,6 +670,70 @@ describe("algorithm set", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("security-key algorithm set", () => {
|
||||
it.each(LEGACY_ALGS.map((a) => [a.name, a] as const))(
|
||||
"%s registers as a second factor",
|
||||
async (_name, alg) => {
|
||||
const keys = alg.keyPair();
|
||||
const credentialId = randomBytes(16);
|
||||
const { options, challengeId } = await createMfaRegistrationOptions(
|
||||
USER_ID,
|
||||
"user@example.test",
|
||||
);
|
||||
const verification = await verifyMfaRegistration(
|
||||
challengeId,
|
||||
USER_ID,
|
||||
registrationResponse(
|
||||
options.challenge,
|
||||
credentialId,
|
||||
alg.cose(keys.publicKey),
|
||||
["usb"],
|
||||
),
|
||||
);
|
||||
expect(verification.verified).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("algorithm lists are passed explicitly", () => {
|
||||
// A library default can move under a runtime change (a post-quantum capable
|
||||
// runtime prepends ML-DSA-44) without any option snapshot on this runtime
|
||||
// noticing, so the arguments themselves are pinned.
|
||||
it("both ceremonies offer EdDSA, ES256, RS256 and accept the full legacy set", async () => {
|
||||
vi.mocked(generateRegistrationOptions).mockClear();
|
||||
vi.mocked(verifyRegistrationResponse).mockClear();
|
||||
const keys = ES256.keyPair();
|
||||
await registerPasskey(ES256, keys, randomBytes(16));
|
||||
const mfa = await createMfaRegistrationOptions(
|
||||
USER_ID,
|
||||
"user@example.test",
|
||||
);
|
||||
await verifyMfaRegistration(
|
||||
mfa.challengeId,
|
||||
USER_ID,
|
||||
registrationResponse(
|
||||
mfa.options.challenge,
|
||||
randomBytes(16),
|
||||
ES256.cose(keys.publicKey),
|
||||
["usb"],
|
||||
),
|
||||
);
|
||||
|
||||
const offered = vi
|
||||
.mocked(generateRegistrationOptions)
|
||||
.mock.calls.map(([opts]) => opts.supportedAlgorithmIDs);
|
||||
const accepted = vi
|
||||
.mocked(verifyRegistrationResponse)
|
||||
.mock.calls.map(([opts]) => opts.supportedAlgorithmIDs);
|
||||
expect(offered).toEqual([
|
||||
[-8, -7, -257],
|
||||
[-8, -7, -257],
|
||||
]);
|
||||
const legacy = LEGACY_ALGS.map((a) => a.alg);
|
||||
expect(accepted).toEqual([legacy, legacy]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 3. Stored credentials ────────────────────────────────────────────
|
||||
|
||||
/** A P-256 key derived from a fixed seed, so its stored bytes are stable. */
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
RP_NAME as rpName,
|
||||
getRpId,
|
||||
getExpectedOrigin,
|
||||
REGISTRATION_ACCEPTED_ALGORITHM_IDS,
|
||||
REGISTRATION_OFFERED_ALGORITHM_IDS,
|
||||
} from "@/lib/auth/webauthn-rp";
|
||||
|
||||
// Boundary narrowing mirrors `passkey.ts`: a malformed body fails fast with a
|
||||
@@ -107,6 +109,7 @@ export async function createMfaRegistrationOptions(
|
||||
rpID: getRpId(),
|
||||
userName: username,
|
||||
attestationType: "none",
|
||||
supportedAlgorithmIDs: REGISTRATION_OFFERED_ALGORITHM_IDS,
|
||||
excludeCredentials: existing.map((c) => ({
|
||||
id: c.credentialId,
|
||||
transports: c.transports as Transport[],
|
||||
@@ -168,6 +171,7 @@ export async function verifyMfaRegistration(
|
||||
expectedChallenge: challenge.challenge,
|
||||
expectedOrigin: getExpectedOrigin(),
|
||||
expectedRPID: getRpId(),
|
||||
supportedAlgorithmIDs: REGISTRATION_ACCEPTED_ALGORITHM_IDS,
|
||||
});
|
||||
} finally {
|
||||
await prisma.authChallenge
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
RP_NAME as rpName,
|
||||
getRpId,
|
||||
getExpectedOrigin,
|
||||
REGISTRATION_ACCEPTED_ALGORITHM_IDS,
|
||||
REGISTRATION_OFFERED_ALGORITHM_IDS,
|
||||
} from "@/lib/auth/webauthn-rp";
|
||||
import { hashToken } from "@/lib/auth/hmac";
|
||||
|
||||
@@ -111,6 +113,7 @@ export async function createRegistrationOptions(
|
||||
rpID: getRpId(),
|
||||
userName: username,
|
||||
attestationType: "none",
|
||||
supportedAlgorithmIDs: REGISTRATION_OFFERED_ALGORITHM_IDS,
|
||||
excludeCredentials: existingPasskeys.map((pk) => ({
|
||||
id: pk.credentialId,
|
||||
transports: pk.transports as Transport[],
|
||||
@@ -217,6 +220,7 @@ export async function verifyRegistration(
|
||||
expectedChallenge,
|
||||
expectedOrigin: getExpectedOrigin(),
|
||||
expectedRPID: getRpId(),
|
||||
supportedAlgorithmIDs: REGISTRATION_ACCEPTED_ALGORITHM_IDS,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,3 +58,23 @@ export function getExpectedOrigin(): string | string[] {
|
||||
const origins = getConfiguredOrigins();
|
||||
return origins.length === 1 ? origins[0] : origins;
|
||||
}
|
||||
|
||||
/**
|
||||
* COSE algorithms offered to the authenticator at registration, in order of
|
||||
* preference: EdDSA, ES256, RS256. Passed explicitly so the options a browser
|
||||
* receives do not follow a library default; a runtime with post-quantum
|
||||
* support would otherwise put ML-DSA-44 at the front of the list.
|
||||
*/
|
||||
export const REGISTRATION_OFFERED_ALGORITHM_IDS = [-8, -7, -257];
|
||||
|
||||
/**
|
||||
* COSE algorithms a registration response may carry. Wider than the offered
|
||||
* list on purpose: it is the set the server has always accepted (EdDSA,
|
||||
* ES256, ES512, PS256/384/512, RS256/384/512 and legacy RS1), so an
|
||||
* authenticator that picks an algorithm it was not offered keeps registering
|
||||
* as it did before. Narrowing it would turn away authenticators that work
|
||||
* today.
|
||||
*/
|
||||
export const REGISTRATION_ACCEPTED_ALGORITHM_IDS = [
|
||||
-8, -7, -36, -37, -38, -39, -257, -258, -259, -65535,
|
||||
];
|
||||
Reference in new issue
Block a user