diff options
4 files changed, 118 insertions, 2 deletions
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 822bdd9..b150aff 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -335,6 +335,40 @@ Deno.test('should throw an error if RP ID not in list of possible RP IDs', async ); }); +Deno.test('should throw an error if type not the expected type', async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + // assertionResponse contains webauthn.get, this should produce an error + expectedType: 'payment.get', + expectedRPID: 'localhost', + authenticator: authenticator, + }), + Error, + 'Unexpected authentication response type', + ); +}); + +Deno.test('should throw an error if type not in list of expected types', async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + // assertionResponse contains webauthn.get, this should produce an error + expectedType: ['payment.get', 'something.get'], + expectedRPID: 'localhost', + authenticator: authenticator, + }), + Error, + 'Unexpected authentication response type', + ); +}); + Deno.test('should pass verification if custom challenge verifier returns true', async () => { const verification = await verifyAuthenticationResponse({ response: { diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 41370a0..c938598 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -18,6 +18,7 @@ export type VerifyAuthenticationResponseOpts = { expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>); expectedOrigin: string | string[]; expectedRPID: string | string[]; + expectedType?: string | string[]; authenticator: AuthenticatorDevice; requireUserVerification?: boolean; advancedFIDOConfig?: { @@ -35,6 +36,7 @@ export type VerifyAuthenticationResponseOpts = { * `generateAuthenticationOptions()` * @param expectedOrigin Website URL (or array of URLs) that the registration should have occurred on * @param expectedRPID RP ID (or array of IDs) that was specified in the registration options + * @param expectedType (Optional) The response type expected ('webauthn.get') * @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID * @param requireUserVerification (Optional) Enforce user verification by the authenticator * (via PIN, fingerprint, etc...) @@ -52,6 +54,7 @@ export async function verifyAuthenticationResponse( expectedChallenge, expectedOrigin, expectedRPID, + expectedType, authenticator, requireUserVerification = true, advancedFIDOConfig, @@ -88,7 +91,16 @@ export async function verifyAuthenticationResponse( const { type, origin, challenge, tokenBinding } = clientDataJSON; // Make sure we're handling an authentication - if (type !== 'webauthn.get') { + if (Array.isArray(expectedType)) { + if (!expectedType.includes(type)) { + const joinedExpectedType = expectedType.join(', '); + throw new Error(`Unexpected authentication response type "${type}", expected one of: ${joinedExpectedType}`); + } + } else if (expectedType) { + if (type !== expectedType) { + throw new Error(`Unexpected authentication response type "${type}", expected "${expectedType}"`); + } + } else if (type !== 'webauthn.get') { throw new Error(`Unexpected authentication response type: ${type}`); } diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 59dbd13..fbe7aed 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -219,6 +219,36 @@ Deno.test('should throw when response origin is not expected value', async () => ); }); +Deno.test('should throw when response type is not expected value', async () => { + await assertRejects( + () => + verifyRegistrationResponse({ + response: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + expectedType: 'something.get' + }), + Error, + 'registration response type', + ); +}); + +Deno.test('should throw when response type is not in list of expected types', async () => { + await assertRejects( + () => + verifyRegistrationResponse({ + response: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + expectedType: ['something.create', 'something.else.create'] + }), + Error, + 'registration response type', + ); +}); + Deno.test('should throw when attestation type is not webauthn.create', async () => { const origin = 'https://dev.dontneeda.pw'; const challenge = attestationNoneChallenge; @@ -250,6 +280,34 @@ Deno.test('should throw when attestation type is not webauthn.create', async () mockDecodeClientData.restore(); }); +Deno.test('should validate when attestation type is not webauthn.create and expected type provided', async () => { + const origin = 'https://dev.dontneeda.pw'; + const challenge = attestationNoneChallenge; + + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + origin, + type: 'webauthn.goodtype', + challenge: attestationNoneChallenge, + }, + ]), + ); + + const verification = await verifyRegistrationResponse({ + response: attestationNone, + expectedChallenge: challenge, + expectedOrigin: origin, + expectedRPID: 'dev.dontneeda.pw', + expectedType: 'webauthn.goodtype' + }); + assert(verification.verified); + + mockDecodeClientData.restore(); +}); + Deno.test('should throw if an unexpected attestation format is specified', async () => { const realAtteObj = decodeAttestationObject( isoBase64URL.toBuffer(attestationNone.response.attestationObject), diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index 154d31a..d2399e8 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -33,6 +33,7 @@ export type VerifyRegistrationResponseOpts = { expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>); expectedOrigin: string | string[]; expectedRPID?: string | string[]; + expectedType?: string | string[]; requireUserVerification?: boolean; supportedAlgorithmIDs?: COSEAlgorithmIdentifier[]; }; @@ -47,6 +48,7 @@ export type VerifyRegistrationResponseOpts = { * `generateRegistrationOptions()` * @param expectedOrigin Website URL (or array of URLs) that the registration should have occurred on * @param expectedRPID RP ID (or array of IDs) that was specified in the registration options + * @param expectedType (Optional) The response type expected ('webauthn.create') * @param requireUserVerification (Optional) Enforce user verification by the authenticator * (via PIN, fingerprint, etc...) * @param supportedAlgorithmIDs Array of numeric COSE algorithm identifiers supported for @@ -60,6 +62,7 @@ export async function verifyRegistrationResponse( expectedChallenge, expectedOrigin, expectedRPID, + expectedType, requireUserVerification = true, supportedAlgorithmIDs = supportedCOSEAlgorithmIdentifiers, } = options; @@ -89,7 +92,16 @@ export async function verifyRegistrationResponse( const { type, origin, challenge, tokenBinding } = clientDataJSON; // Make sure we're handling an registration - if (type !== 'webauthn.create') { + if (Array.isArray(expectedType)) { + if (!expectedType.includes(type)) { + const joinedExpectedType = expectedType.join(', '); + throw new Error(`Unexpected registration response type "${type}", expected one of: ${joinedExpectedType}`); + } + } else if (expectedType) { + if (type !== expectedType) { + throw new Error(`Unexpected registration response type "${type}", expected "${expectedType}"`); + } + } else if (type !== 'webauthn.create') { throw new Error(`Unexpected registration response type: ${type}`); } |