From e42b19a86400334f4767212333497e2aed6557af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:03:46 +0000 Subject: [PATCH 1/4] Initial plan From 81eddfa206b75c7f4060927361f076eebec0206e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:11:40 +0000 Subject: [PATCH 2/4] fix: only require UV flag when userVerification is "required" per WebAuthn spec The WebAuthn spec (step 16) says the UV flag should only be verified when the Relying Party explicitly requires user verification. Previously, factor "first" always required UV. Now UV is only required when userVerification is set to "required" in the expected options. Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- lib/main.js | 17 ++++++++++---- test/main.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/lib/main.js b/lib/main.js index 754d0f57..4b22fc86 100644 --- a/lib/main.js +++ b/lib/main.js @@ -538,12 +538,15 @@ class Fido2Lib { * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable + * @param {String} [expected.userVerification] The expected user verification requirement. Valid values are "required", "preferred", or "discouraged". + * If "required", the authenticator must have performed user verification and the UV flag is checked. * @return {Promise} Returns a Promise that resolves to a {@link Fido2AttestationResult} * @throws {Error} If parsing or validation fails */ async attestationResult(res, expected) { - expected.flags = factorToFlags(expected.factor, ["AT"]); + expected.flags = factorToFlags(expected.factor, ["AT"], expected.userVerification); delete expected.factor; + delete expected.userVerification; return await Fido2AttestationResult.create(res, expected); } @@ -565,6 +568,8 @@ class Fido2Lib { * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable + * @param {String} [expected.userVerification] The expected user verification requirement. Valid values are "required", "preferred", or "discouraged". + * If "required", the authenticator must have performed user verification and the UV flag is checked. * @param {String} expected.publicKey A PEM encoded public key that will be used to validate the assertion response signature. * This is the public key that was returned for this user during [attestationResult]{@link Fido2Lib#attestationResult} * @param {Number} expected.prevCounter The previous value of the signature counter for this authenticator. @@ -574,8 +579,9 @@ class Fido2Lib { */ // deno-lint-ignore require-await async assertionResult(res, expected) { - expected.flags = factorToFlags(expected.factor, []); + expected.flags = factorToFlags(expected.factor, [], expected.userVerification); delete expected.factor; + delete expected.userVerification; return Fido2AssertionResult.create(res, expected); } @@ -818,14 +824,15 @@ function setOpt(obj, prop, val) { */ -function factorToFlags(expectedFactor, flags) { - // var flags = ["AT"]; +function factorToFlags(expectedFactor, flags, userVerification) { flags = flags || []; switch (expectedFactor) { case "first": flags.push("UP"); - flags.push("UV"); + if (userVerification === "required") { + flags.push("UV"); + } break; case "second": flags.push("UP"); diff --git a/test/main.test.js b/test/main.test.js index e494786b..33f691c3 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -754,6 +754,66 @@ describe("Fido2Lib", function() { return res; }); }); + + it("valid assertion with factor first and no userVerification does not require UV", function() { + const expectations = { + challenge: "eaTyUNnyPDDdK8SNEgTEUvz1Q8dylkjjTimYd5X7QAo-F8_Z1lsJi3BilUpFZHkICNDWY8r9ivnTgW7-XZC3qQ", + origin: "https://localhost:8443", + factor: "first", + publicKey: h.lib.assnPublicKey, + prevCounter: 362, + userHandle: null, + }; + + return serv.assertionResult(h.lib.assertionResponse, expectations) + .then((res) => { + assert.instanceOf(res, Fido2AssertionResult); + return res; + }); + }); + + it("valid assertion with factor first and userVerification required", function() { + const expectations = { + challenge: "g_Pu32bpluktxugNNBLX-ZO5N9ub0D50bJERbKiU2GWON3md0rR9CaQYdPHdCgo-dpi1-9gbJJvmCuHDnh04Rg", + origin: "https://mighty-fireant-84.loca.lt", + factor: "first", + userVerification: "required", + publicKey: "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0dBhdNNvh2NkaNstlFhrBhi9yrjP\n" + + "0qPqZvRRnf3zQiN9zDwJ9ZXoyO4dhKz3OIhMBJG6F+muH35fEsWBZI6dhg==\n" + + "-----END PUBLIC KEY-----\n", + prevCounter: 0, + userHandle: null, + }; + + let assertionResponse = { + rawId: coerceToArrayBuffer("7S8aQSSxqPkztahKbgw36Mr_-hE", "rawId"), + response: { + authenticatorData: coerceToArrayBuffer("YS67HU8UTNyqQ5f-EVzitWw5paVnpyhQli2ahN6PS6UFAAAAAA", "authenticatorData"), + clientDataJSON: coerceToArrayBuffer("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZ19QdTMyYnBsdWt0eHVnTk5CTFgtWk81Tjl1YjBENTBiSkVSYktpVTJHV09OM21kMHJSOUNhUVlkUEhkQ2dvLWRwaTEtOWdiSkp2bUN1SERuaDA0UmciLCJvcmlnaW4iOiJodHRwczovL21pZ2h0eS1maXJlYW50LTg0LmxvY2EubHQifQ", "clientDataJSON"), + signature: coerceToArrayBuffer("MEQCIEhIhQBglBn1iGMDgF4WFDG7ISJHD1C1Q60drTaijjV2AiBOnQleadMnzcMJ0EBpwoP8zr2V5lBuKvpNfJrcbC1T4w", "signature"), + }, + }; + + return serv.assertionResult(assertionResponse, expectations).then((res) => { + assert.instanceOf(res, Fido2AssertionResult); + return res; + }); + }); + + it("rejects assertion with factor first and userVerification required when UV not set", function() { + const expectations = { + challenge: "eaTyUNnyPDDdK8SNEgTEUvz1Q8dylkjjTimYd5X7QAo-F8_Z1lsJi3BilUpFZHkICNDWY8r9ivnTgW7-XZC3qQ", + origin: "https://localhost:8443", + factor: "first", + userVerification: "required", + publicKey: h.lib.assnPublicKey, + prevCounter: 362, + userHandle: null, + }; + + return assert.isRejected(serv.assertionResult(h.lib.assertionResponse, expectations), Error, "expected flag was not set: UV"); + }); }); describe("addAttestationFormat", function() { From 5d5dec82da35cd7d6aafb08ec9041d6b54e4fe0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:33:19 +0000 Subject: [PATCH 3/4] Address review: update JSDoc, TypeScript types, and add attestation tests - Update factor JSDoc in attestationResult/assertionResult to clarify UV is only checked when userVerification is "required" - Update factorToFlags JSDoc to document the userVerification parameter - Add userVerification to TypeScript ExpectedAttestationResult and ExpectedAssertionResult interfaces - Add attestation tests for factor "first" with/without userVerification Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- lib/main.js | 15 +++++++++------ test/main.test.js | 34 ++++++++++++++++++++++++++++++++++ types/main.d.ts | 2 ++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/main.js b/lib/main.js index 4b22fc86..040a16ae 100644 --- a/lib/main.js +++ b/lib/main.js @@ -535,7 +535,8 @@ class Fido2Lib { * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions} * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org" * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either". - * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). + * If "first", this requires that the authenticator performed user presence (UP). User verification (UV) is only + * checked if `expected.userVerification` is set to `"required"`. * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable * @param {String} [expected.userVerification] The expected user verification requirement. Valid values are "required", "preferred", or "discouraged". @@ -565,7 +566,8 @@ class Fido2Lib { * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions} * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org" * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either". - * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). + * If "first", this requires that the authenticator performed user presence (UP). User verification (UV) is only + * checked if `expected.userVerification` is set to `"required"`. * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable * @param {String} [expected.userVerification] The expected user verification requirement. Valid values are "required", "preferred", or "discouraged". @@ -817,13 +819,14 @@ function setOpt(obj, prop, val) { } /** - * @param {string} expectedFactor - "first" | "second" | "either" + * Converts a factor string and optional userVerification requirement to an array of expected flag strings. * See {@link https://www.w3.org/TR/webauthn-3/#authdata-flags Flags Docs on W3} * - * @param {Array} flags array of flag strings + * @param {string} expectedFactor - "first" | "second" | "either" + * @param {Array} flags - array of flag strings to append to + * @param {string} [userVerification] - "required" | "preferred" | "discouraged". + * When "required", the UV flag is added for factor "first" per WebAuthn spec step 16. */ - - function factorToFlags(expectedFactor, flags, userVerification) { flags = flags || []; diff --git a/test/main.test.js b/test/main.test.js index 33f691c3..f738b7a4 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -589,6 +589,40 @@ describe("Fido2Lib", function() { }); it("catches bad requests"); + + it("validates attestation with factor first and no userVerification does not require UV", async function() { + const expectations = { + challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w", + origin: "https://localhost:8443", + factor: "first", + }; + + const res = await serv.attestationResult( + h.lib.makeCredentialAttestationNoneResponse, + expectations, + ); + + assert.instanceOf(res, Fido2AttestationResult); + return res; + }); + + it("rejects attestation with factor first and userVerification required when UV not set", async function() { + const expectations = { + challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w", + origin: "https://localhost:8443", + factor: "first", + userVerification: "required", + }; + + return assert.isRejected( + serv.attestationResult( + h.lib.makeCredentialAttestationNoneResponse, + expectations, + ), + Error, + "expected flag was not set: UV", + ); + }); }); describe("assertionOptions", function() { diff --git a/types/main.d.ts b/types/main.d.ts index 351056ff..91856eb7 100644 --- a/types/main.d.ts +++ b/types/main.d.ts @@ -98,6 +98,7 @@ declare module "fido2-lib" { origin: string; challenge: string; factor: Factor; + userVerification?: UserVerification; } interface Fido2AttestationResult { @@ -153,6 +154,7 @@ declare module "fido2-lib" { publicKey: string; prevCounter: number; userHandle: string | null; + userVerification?: UserVerification; allowCredentials?: PublicKeyCredentialDescriptor[]; } From eed95761de56971a54d8c843c1818a54253b3149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:35:05 +0000 Subject: [PATCH 4/4] Fix factorToFlags JSDoc description per code review Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com> --- lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.js b/lib/main.js index 040a16ae..2529fc91 100644 --- a/lib/main.js +++ b/lib/main.js @@ -819,7 +819,7 @@ function setOpt(obj, prop, val) { } /** - * Converts a factor string and optional userVerification requirement to an array of expected flag strings. + * Appends expected flags to an array based on factor string and optional userVerification requirement. * See {@link https://www.w3.org/TR/webauthn-3/#authdata-flags Flags Docs on W3} * * @param {string} expectedFactor - "first" | "second" | "either"