diff --git a/lib/main.js b/lib/main.js index 754d0f5..2529fc9 100644 --- a/lib/main.js +++ b/lib/main.js @@ -535,15 +535,19 @@ 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". + * 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); } @@ -562,9 +566,12 @@ 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". + * 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 +581,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); } @@ -811,21 +819,23 @@ function setOpt(obj, prop, val) { } /** - * @param {string} expectedFactor - "first" | "second" | "either" + * 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 {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) { - // 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 e494786..f738b7a 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() { @@ -754,6 +788,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() { diff --git a/types/main.d.ts b/types/main.d.ts index 351056f..91856eb 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[]; }