summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2020-06-07 14:24:05 -0700
committerMatthew Miller <matthew@millerti.me>2020-06-07 14:24:05 -0700
commitf1c242745df883709d10276fca08eccdecc28f6b (patch)
tree84d58b5a0e27cb6a6ae518124dfcc67e14b953fd
parent9bc56318015a0b8cc11e9222c3c84f8b2119ce17 (diff)
Enable requiring user verification while verifying
-rw-r--r--packages/server/src/assertion/verifyAssertionResponse.test.ts30
-rw-r--r--packages/server/src/assertion/verifyAssertionResponse.ts23
-rw-r--r--packages/server/src/attestation/verifyAttestationResponse.test.ts22
-rw-r--r--packages/server/src/attestation/verifyAttestationResponse.ts20
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');
}