diff options
author | Matthew Miller <matthew@millerti.me> | 2021-01-21 14:07:41 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-21 14:07:41 -0800 |
commit | c7bce757ebb64dfe856e62326b2045761b70ea48 (patch) | |
tree | 9ba5e07bb8e6aeb072d63287d86ada37b45a2d74 | |
parent | 9ad5f7d214a707af9c3f9d6d078323e41ce1a017 (diff) | |
parent | 5fa62a7f182028ddb25d4ae450756e2f290efbb7 (diff) |
Merge pull request #91 from MasterKale/feature/multiple-origins
feature/multiple-origins
5 files changed, 152 insertions, 20 deletions
diff --git a/package.json b/package.json index 6d13b34..d013745 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,14 @@ "@typescript-eslint/parser": "^3.10.1", "eslint": "^7.8.1", "husky": "^4.3.0", - "jest": "^25.1.0", + "jest": "^26.6.3", "jest-environment-jsdom": "^26.3.0", "lerna": "^3.22.1", "lint-staged": "^10.3.0", "prettier": "^2.1.1", "rimraf": "^3.0.2", "semver": "^7.3.2", - "ts-jest": "^25.5.1", + "ts-jest": "^26.4.4", "ts-morph": "^9.0.0", "ts-node": "^8.10.2", "ttypescript": "^1.5.12", diff --git a/packages/server/src/assertion/verifyAssertionResponse.test.ts b/packages/server/src/assertion/verifyAssertionResponse.test.ts index 3f97959..4e4b64f 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.test.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.test.ts @@ -207,6 +207,54 @@ test.skip('should verify TPM assertion', () => { expect(verification.verified).toEqual(true); }); +test('should support multiple possible origins', () => { + const verification = verifyAssertionResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: ['https://simplewebauthn.dev', assertionOrigin], + expectedRPID: 'dev.dontneeda.pw', + authenticator: authenticator, + }); + + expect(verification.verified).toEqual(true); +}); + +test('should throw an error if origin not in list of expected origins', async () => { + expect(() => { + verifyAssertionResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: ['https://simplewebauthn.dev', 'https://fizz.buzz'], + expectedRPID: 'dev.dontneeda.pw', + authenticator: authenticator, + }); + }).toThrow(/unexpected assertion origin/i); +}); + +test('should support multiple possible RP IDs', async () => { + const verification = verifyAssertionResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: ['dev.dontneeda.pw', 'simplewebauthn.dev'], + authenticator: authenticator, + }); + + expect(verification.verified).toEqual(true); +}); + +test('should throw an error if RP ID not in list of possible RP IDs', async () => { + expect(() => { + verifyAssertionResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: ['simplewebauthn.dev'], + authenticator: authenticator, + }); + }).toThrow(/unexpected rp id/i); +}); + /** * Assertion examples below */ diff --git a/packages/server/src/assertion/verifyAssertionResponse.ts b/packages/server/src/assertion/verifyAssertionResponse.ts index 993b7e9..0c76fae 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.ts @@ -15,8 +15,8 @@ import isBase64URLString from '../helpers/isBase64URLString'; type Options = { credential: AssertionCredentialJSON; expectedChallenge: string; - expectedOrigin: string; - expectedRPID: string; + expectedOrigin: string | string[]; + expectedRPID: string | string[]; authenticator: AuthenticatorDevice; fidoUserVerification?: UserVerificationRequirement; }; @@ -29,8 +29,8 @@ type Options = { * @param credential Authenticator credential returned by browser's `startAssertion()` * @param expectedChallenge The base64url-encoded `options.challenge` returned by * `generateAssertionOptions()` - * @param expectedOrigin Website URL that the attestation should have occurred on - * @param expectedRPID RP ID that was specified in the attestation options + * @param expectedOrigin Website URL (or array of URLs) that the attestation should have occurred on + * @param expectedRPID RP ID (or array of IDs) that was specified in the attestation options * @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID * @param fidoUserVerification (Optional) The value specified for `userVerification` when calling * `generateAssertionOptions()`. Activates FIDO-specific user presence and verification checks. @@ -87,8 +87,16 @@ export default function verifyAssertionResponse(options: Options): VerifiedAsser } // Check that the origin is our site - if (origin !== expectedOrigin) { - throw new Error(`Unexpected assertion origin "${origin}", expected "${expectedOrigin}"`); + if (Array.isArray(expectedOrigin)) { + if (!expectedOrigin.includes(origin)) { + throw new Error( + `Unexpected assertion origin "${origin}", expected one of: ${expectedOrigin.join(', ')}`, + ); + } + } else { + if (origin !== expectedOrigin) { + throw new Error(`Unexpected assertion origin "${origin}", expected "${expectedOrigin}"`); + } } if (!isBase64URLString(response.authenticatorData)) { @@ -118,9 +126,21 @@ export default function verifyAssertionResponse(options: Options): VerifiedAsser const { rpIdHash, flags, counter } = parsedAuthData; // Make sure the response's RP ID is ours - const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii')); - if (!rpIdHash.equals(expectedRPIDHash)) { - throw new Error(`Unexpected RP ID hash`); + if (typeof expectedRPID === 'string') { + const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii')); + if (!rpIdHash.equals(expectedRPIDHash)) { + throw new Error(`Unexpected RP ID hash`); + } + } else { + // Go through each expected RP ID and try to find one that matches + const foundMatch = expectedRPID.some(expected => { + const expectedRPIDHash = toHash(Buffer.from(expected, 'ascii')); + return rpIdHash.equals(expectedRPIDHash); + }); + + if (!foundMatch) { + throw new Error(`Unexpected RP ID hash`); + } } // Enforce user verification if required diff --git a/packages/server/src/attestation/verifyAttestationResponse.test.ts b/packages/server/src/attestation/verifyAttestationResponse.test.ts index 99bf653..a6e0814 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.test.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.test.ts @@ -438,6 +438,50 @@ test('should validate Android-Key response', async () => { ); }); +test('should support multiple possible origins', async () => { + const verification = await verifyAttestationResponse({ + credential: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: ['https://dev.dontneeda.pw', 'https://different.address'], + expectedRPID: 'dev.dontneeda.pw', + }); + + expect(verification.verified).toBe(true); +}); + +test('should throw an error if origin not in list of expected origins', async () => { + await expect( + verifyAttestationResponse({ + credential: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: ['https://different.address'], + expectedRPID: 'dev.dontneeda.pw', + }), + ).rejects.toThrow(/unexpected attestation origin/i); +}); + +test('should support multiple possible RP IDs', async () => { + const verification = await verifyAttestationResponse({ + credential: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: ['dev.dontneeda.pw', 'simplewebauthn.dev'], + }); + + expect(verification.verified).toBe(true); +}); + +test('should throw an error if RP ID not in list of possible RP IDs', async () => { + await expect( + verifyAttestationResponse({ + credential: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: ['simplewebauthn.dev'], + }), + ).rejects.toThrow(/unexpected rp id/i); +}); + /** * Various Attestations Below */ diff --git a/packages/server/src/attestation/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts index 8a213f0..3940ea6 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.ts @@ -22,8 +22,8 @@ import verifyApple from './verifications/verifyApple'; type Options = { credential: AttestationCredentialJSON; expectedChallenge: string; - expectedOrigin: string; - expectedRPID?: string; + expectedOrigin: string | string[]; + expectedRPID?: string | string[]; requireUserVerification?: boolean; supportedAlgorithmIDs?: COSEAlgorithmIdentifier[]; }; @@ -36,8 +36,8 @@ type Options = { * @param credential Authenticator credential returned by browser's `startAttestation()` * @param expectedChallenge The base64url-encoded `options.challenge` returned by * `generateAttestationOptions()` - * @param expectedOrigin Website URL that the attestation should have occurred on - * @param expectedRPID RP ID that was specified in the attestation options + * @param expectedOrigin Website URL (or array of URLs) that the attestation should have occurred on + * @param expectedRPID RP ID (or array of IDs) that was specified in the attestation options * @param requireUserVerification (Optional) Enforce user verification by the authenticator * (via PIN, fingerprint, etc...) * @param supportedAlgorithmIDs Array of numeric COSE algorithm identifiers supported for @@ -88,8 +88,16 @@ export default async function verifyAttestationResponse( } // Check that the origin is our site - if (origin !== expectedOrigin) { - throw new Error(`Unexpected attestation origin "${origin}", expected "${expectedOrigin}"`); + if (Array.isArray(expectedOrigin)) { + if (!expectedOrigin.includes(origin)) { + throw new Error( + `Unexpected attestation origin "${origin}", expected one of: ${expectedOrigin.join(', ')}`, + ); + } + } else { + if (origin !== expectedOrigin) { + throw new Error(`Unexpected attestation origin "${origin}", expected "${expectedOrigin}"`); + } } if (tokenBinding) { @@ -110,9 +118,21 @@ export default async function verifyAttestationResponse( // Make sure the response's RP ID is ours if (expectedRPID) { - const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii')); - if (!rpIdHash.equals(expectedRPIDHash)) { - throw new Error(`Unexpected RP ID hash`); + if (typeof expectedRPID === 'string') { + const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii')); + if (!rpIdHash.equals(expectedRPIDHash)) { + throw new Error(`Unexpected RP ID hash`); + } + } else { + // Go through each expected RP ID and try to find one that matches + const foundMatch = expectedRPID.some(expected => { + const expectedRPIDHash = toHash(Buffer.from(expected, 'ascii')); + return rpIdHash.equals(expectedRPIDHash); + }); + + if (!foundMatch) { + throw new Error(`Unexpected RP ID hash`); + } } } |