diff options
author | Matthew Miller <matthew@millerti.me> | 2020-06-07 14:24:05 -0700 |
---|---|---|
committer | Matthew Miller <matthew@millerti.me> | 2020-06-07 14:24:05 -0700 |
commit | f1c242745df883709d10276fca08eccdecc28f6b (patch) | |
tree | 84d58b5a0e27cb6a6ae518124dfcc67e14b953fd | |
parent | 9bc56318015a0b8cc11e9222c3c84f8b2119ce17 (diff) |
Enable requiring user verification while verifying
4 files changed, 89 insertions, 6 deletions
diff --git a/packages/server/src/assertion/verifyAssertionResponse.test.ts b/packages/server/src/assertion/verifyAssertionResponse.test.ts index 923e62f..20b6e0e 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.test.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.test.ts @@ -1,3 +1,4 @@ +import base64url from 'base64url'; import verifyAssertionResponse from './verifyAssertionResponse'; import * as decodeClientDataJSON from '../helpers/decodeClientDataJSON'; @@ -150,6 +151,35 @@ test('should not compare counters if both are 0', () => { expect(verification.verified).toEqual(true); }); +test('should throw an error if user verification is required but user was not verified', () => { + const actualData = parseAuthenticatorData.default( + base64url.toBuffer(assertionResponse.response.authenticatorData), + ); + + mockParseAuthData.mockReturnValue({ + ...actualData, + flags: { + up: true, + uv: false, + }, + }); + + expect(() => { + verifyAssertionResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + authenticator: authenticator, + requireUserVerification: true, + }); + }).toThrow(/user could not be verified/i); +}); + +/** + * Assertion examples below + */ + const assertionResponse = { id: 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew', rawId: '', diff --git a/packages/server/src/assertion/verifyAssertionResponse.ts b/packages/server/src/assertion/verifyAssertionResponse.ts index 93754c7..9dedc2d 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.ts @@ -13,6 +13,7 @@ type Options = { expectedOrigin: string; expectedRPID: string; authenticator: AuthenticatorDevice; + requireUserVerification?: boolean; }; /** @@ -20,13 +21,24 @@ type Options = { * * **Options:** * - * @param response Authenticator assertion response with base64url-encoded values + * @param credential Authenticator credential returned by browser's `startAssertion()` * @param expectedChallenge The random value provided to generateAssertionOptions for the * authenticator to sign - * @param expectedOrigin Expected URL of website assertion should have occurred on + * @param expectedOrigin Website URL that the attestation should have occurred on + * @param expectedRPID RP ID that was specified in the attestation options + * @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID + * @param requireUserVerification (Optional) Enforce user verification by the authenticator + * (via PIN, fingerprint, etc...) */ export default function verifyAssertionResponse(options: Options): VerifiedAssertion { - const { credential, expectedChallenge, expectedOrigin, expectedRPID, authenticator } = options; + const { + credential, + expectedChallenge, + expectedOrigin, + expectedRPID, + authenticator, + requireUserVerification = false, + } = options; const { response } = credential; const clientDataJSON = decodeClientDataJSON(response.clientDataJSON); @@ -62,6 +74,11 @@ export default function verifyAssertionResponse(options: Options): VerifiedAsser throw new Error('User not present during assertion'); } + // Enforce user verification if specified + if (requireUserVerification && !flags.uv) { + throw new Error('User verification required, but user could not be verified'); + } + const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON)); const signatureBase = Buffer.concat([rpIdHash, flagsBuf, counterBuf, clientDataHash]); diff --git a/packages/server/src/attestation/verifyAttestationResponse.test.ts b/packages/server/src/attestation/verifyAttestationResponse.test.ts index 24ec2db..b2ff37c 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.test.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.test.ts @@ -1,3 +1,5 @@ +import base64url from 'base64url'; + import verifyAttestationResponse from './verifyAttestationResponse'; import * as decodeAttestationObject from '../helpers/decodeAttestationObject'; @@ -312,6 +314,26 @@ test('should not include authenticator info if not verified', () => { expect(verification.authenticatorInfo).toBeUndefined(); }); +test('should throw an error if user verification is required but user was not verified', () => { + mockParseAuthData.mockReturnValue({ + rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')), + flags: { + up: true, + uv: false, + }, + }); + + expect(() => { + const verification = verifyAttestationResponse({ + credential: attestationFIDOU2F, + expectedChallenge: attestationFIDOU2FChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + requireUserVerification: true, + }); + }).toThrow(/user could not be verified/i); +}); + /** * Various Attestations Below */ diff --git a/packages/server/src/attestation/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts index 889b034..6779244 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.ts @@ -18,6 +18,7 @@ type Options = { expectedChallenge: string; expectedOrigin: string; expectedRPID: string; + requireUserVerification?: boolean; }; /** @@ -28,11 +29,19 @@ type Options = { * @param response Authenticator attestation response with base64url-encoded values * @param expectedChallenge The random value provided to generateAttestationOptions for the * authenticator to sign - * @param expectedOrigin Expected URL of website attestation should have occurred on - * @param expectedRPID Expect RP ID as it was specified in the attestation options + * @param expectedOrigin Website URL that the attestation should have occurred on + * @param expectedRPID RP ID that was specified in the attestation options + * @param requireUserVerification (Optional) Enforce user verification by the authenticator + * (via PIN, fingerprint, etc...) */ export default function verifyAttestationResponse(options: Options): VerifiedAttestation { - const { credential, expectedChallenge, expectedOrigin, expectedRPID } = options; + const { + credential, + expectedChallenge, + expectedOrigin, + expectedRPID, + requireUserVerification = false, + } = options; const { response } = credential; const clientDataJSON = decodeClientDataJSON(response.clientDataJSON); @@ -72,6 +81,11 @@ export default function verifyAttestationResponse(options: Options): VerifiedAtt throw new Error('User not present during assertion'); } + // Enforce user verification if specified + if (requireUserVerification && !flags.uv) { + throw new Error('User verification required, but user could not be verified'); + } + if (!credentialID) { throw new Error('No credential ID was provided by authenticator'); } |