Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add expectedType for verifyAuthenticationResponse and verifyRegistrationResponse #436

Merged
merged 3 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand All @@ -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...)
Expand All @@ -52,6 +54,7 @@ export async function verifyAuthenticationResponse(
expectedChallenge,
expectedOrigin,
expectedRPID,
expectedType,
authenticator,
requireUserVerification = true,
advancedFIDOConfig,
Expand Down Expand Up @@ -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}`);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 13 additions & 1 deletion packages/server/src/registration/verifyRegistrationResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Expand All @@ -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
Expand All @@ -60,6 +62,7 @@ export async function verifyRegistrationResponse(
expectedChallenge,
expectedOrigin,
expectedRPID,
expectedType,
requireUserVerification = true,
supportedAlgorithmIDs = supportedCOSEAlgorithmIdentifiers,
} = options;
Expand Down Expand Up @@ -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}`);
}

Expand Down