diff options
author | Matthew Miller <matthew@millerti.me> | 2020-05-26 22:38:27 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-26 22:38:27 -0700 |
commit | 5a1acc9128be6dbce3d8d718defe93fb14a88e36 (patch) | |
tree | 7eea60332475794ba7fa02ea24374fc98b59baf4 | |
parent | 2374c09f8444b8a80ce8429f2654ce1b3b92c346 (diff) | |
parent | 1c956b9c39f5c175d3841b1fafaeeb495d1eea6b (diff) |
Merge pull request #16 from MasterKale/feature/support-more-credential-options
feature/support-more-credential-options
9 files changed, 215 insertions, 216 deletions
diff --git a/packages/browser/src/methods/startAssertion.test.ts b/packages/browser/src/methods/startAssertion.test.ts index 7449dbf..db401dc 100644 --- a/packages/browser/src/methods/startAssertion.test.ts +++ b/packages/browser/src/methods/startAssertion.test.ts @@ -22,17 +22,15 @@ const mockSignature = toBase64String(toUint8Array('mockSignature')); const mockUserHandle = toBase64String(toUint8Array('mockUserHandle')); const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { - publicKey: { - challenge: 'fizz', - allowCredentials: [ - { - id: 'abcdefgfdnsdfunguisdfgs', - type: 'public-key', - transports: ['nfc'], - }, - ], - timeout: 1, - }, + challenge: 'fizz', + allowCredentials: [ + { + id: 'abcdefgfdnsdfunguisdfgs', + type: 'public-key', + transports: ['nfc'], + }, + ], + timeout: 1, }; beforeEach(() => { @@ -57,7 +55,7 @@ test('should convert options before passing to navigator.credentials.get(...)', const argsPublicKey = mockNavigatorGet.mock.calls[0][0].publicKey; const credId = base64js.fromByteArray(argsPublicKey.allowCredentials[0].id); - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.publicKey.challenge)); + expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); // Make sure the credential ID is a proper base64 with a length that's a multiple of 4 expect(credId.length % 4).toEqual(0); diff --git a/packages/browser/src/methods/startAssertion.ts b/packages/browser/src/methods/startAssertion.ts index 0648e56..093cf30 100644 --- a/packages/browser/src/methods/startAssertion.ts +++ b/packages/browser/src/methods/startAssertion.ts @@ -23,9 +23,9 @@ export default async function startAssertion( // We need to convert some values to Uint8Arrays before passing the credentials to the navigator const publicKey: PublicKeyCredentialRequestOptions = { - ...requestOptionsJSON.publicKey, - challenge: toUint8Array(requestOptionsJSON.publicKey.challenge), - allowCredentials: requestOptionsJSON.publicKey.allowCredentials.map( + ...requestOptionsJSON, + challenge: toUint8Array(requestOptionsJSON.challenge), + allowCredentials: requestOptionsJSON.allowCredentials.map( toPublicKeyCredentialDescriptor, ), }; diff --git a/packages/browser/src/methods/startAttestation.test.ts b/packages/browser/src/methods/startAttestation.test.ts index a972014..ae79235 100644 --- a/packages/browser/src/methods/startAttestation.test.ts +++ b/packages/browser/src/methods/startAttestation.test.ts @@ -19,31 +19,29 @@ const mockAttestationObject = 'mockAtte'; const mockClientDataJSON = 'mockClie'; const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { - publicKey: { - challenge: 'fizz', - attestation: 'direct', - pubKeyCredParams: [ - { - alg: -7, - type: 'public-key', - }, - ], - rp: { - id: '1234', - name: 'simplewebauthn', - }, - user: { - id: '5678', - displayName: 'username', - name: 'username', - }, - timeout: 1, - excludeCredentials: [{ - id: 'authIdentifier', + challenge: 'fizz', + attestation: 'direct', + pubKeyCredParams: [ + { + alg: -7, type: 'public-key', - transports: ['internal'], - }], + }, + ], + rp: { + id: '1234', + name: 'simplewebauthn', }, + user: { + id: '5678', + displayName: 'username', + name: 'username', + }, + timeout: 1, + excludeCredentials: [{ + id: 'authIdentifier', + type: 'public-key', + transports: ['internal'], + }], }; beforeEach(() => { @@ -67,8 +65,8 @@ test('should convert options before passing to navigator.credentials.create(...) const argsPublicKey = mockNavigatorCreate.mock.calls[0][0].publicKey; - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.publicKey.challenge)); - expect(argsPublicKey.user.id).toEqual(toUint8Array(goodOpts1.publicKey.user.id)); + expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); + expect(argsPublicKey.user.id).toEqual(toUint8Array(goodOpts1.user.id)); expect(argsPublicKey.excludeCredentials).toEqual([{ id: base64js.toByteArray('authIdentifier=='), type: 'public-key', diff --git a/packages/browser/src/methods/startAttestation.ts b/packages/browser/src/methods/startAttestation.ts index f77c2de..ea05fa8 100644 --- a/packages/browser/src/methods/startAttestation.ts +++ b/packages/browser/src/methods/startAttestation.ts @@ -23,13 +23,13 @@ export default async function startAttestation( // We need to convert some values to Uint8Arrays before passing the credentials to the navigator const publicKey: PublicKeyCredentialCreationOptions = { - ...creationOptionsJSON.publicKey, - challenge: toUint8Array(creationOptionsJSON.publicKey.challenge), + ...creationOptionsJSON, + challenge: toUint8Array(creationOptionsJSON.challenge), user: { - ...creationOptionsJSON.publicKey.user, - id: toUint8Array(creationOptionsJSON.publicKey.user.id), + ...creationOptionsJSON.user, + id: toUint8Array(creationOptionsJSON.user.id), }, - excludeCredentials: creationOptionsJSON.publicKey.excludeCredentials.map( + excludeCredentials: creationOptionsJSON.excludeCredentials.map( toPublicKeyCredentialDescriptor, ), }; diff --git a/packages/server/src/assertion/generateAssertionOptions.test.ts b/packages/server/src/assertion/generateAssertionOptions.test.ts index 257657b..aa345af 100644 --- a/packages/server/src/assertion/generateAssertionOptions.test.ts +++ b/packages/server/src/assertion/generateAssertionOptions.test.ts @@ -3,40 +3,41 @@ import generateAssertionOptions from './generateAssertionOptions'; test('should generate credential request options suitable for sending via JSON', () => { const challenge = 'totallyrandomvalue'; - const options = generateAssertionOptions( + const options = generateAssertionOptions({ challenge, - 1, - [ + timeout: 1, + allowedBase64CredentialIDs: [ Buffer.from('1234', 'ascii').toString('base64'), Buffer.from('5678', 'ascii').toString('base64'), ], - ); + }); expect(options).toEqual({ - publicKey: { - challenge, - allowCredentials: [ - { - id: 'MTIzNA==', - type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'], - }, - { - id: 'NTY3OA==', - type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'], - }, - ], - timeout: 1, - }, + challenge, + allowCredentials: [ + { + id: 'MTIzNA==', + type: 'public-key', + transports: ['usb', 'ble', 'nfc', 'internal'], + }, + { + id: 'NTY3OA==', + type: 'public-key', + transports: ['usb', 'ble', 'nfc', 'internal'], + }, + ], + timeout: 1, }); }); test('defaults to 60 seconds if no timeout is specified', () => { - const options = generateAssertionOptions('totallyrandomvalue', undefined, [ - Buffer.from('1234', 'ascii').toString('base64'), - Buffer.from('5678', 'ascii').toString('base64'), - ]); + const options = generateAssertionOptions({ + challenge: 'totallyrandomvalue', + allowedBase64CredentialIDs: [ + Buffer.from('1234', 'ascii').toString('base64'), + Buffer.from('5678', 'ascii').toString('base64'), + ], + }); - expect(options.publicKey.timeout).toEqual(60000); + expect(options.timeout).toEqual(60000); }); diff --git a/packages/server/src/assertion/generateAssertionOptions.ts b/packages/server/src/assertion/generateAssertionOptions.ts index b30344d..b31a34f 100644 --- a/packages/server/src/assertion/generateAssertionOptions.ts +++ b/packages/server/src/assertion/generateAssertionOptions.ts @@ -2,6 +2,13 @@ import type { PublicKeyCredentialRequestOptionsJSON, } from '@simplewebauthn/typescript-types'; +type Options = { + challenge: string, + allowedBase64CredentialIDs: string[], + suggestedTransports?: AuthenticatorTransport[], + timeout?: number, +}; + /** * Prepare a value to pass into navigator.credentials.get(...) for authenticator "login" * @@ -12,20 +19,22 @@ import type { * @param suggestedTransports Suggested types of authenticators for assertion */ export default function generateAssertionOptions( - challenge: string, - timeout = 60000, - allowedBase64CredentialIDs: string[], - suggestedTransports: AuthenticatorTransport[] = ['usb', 'ble', 'nfc', 'internal'], + options: Options, ): PublicKeyCredentialRequestOptionsJSON { + const { + challenge, + allowedBase64CredentialIDs, + suggestedTransports = ['usb', 'ble', 'nfc', 'internal'], + timeout = 60000, + } = options; + return { - publicKey: { - challenge, - allowCredentials: allowedBase64CredentialIDs.map(id => ({ - id, - type: 'public-key', - transports: suggestedTransports, - })), - timeout, - }, + challenge, + allowCredentials: allowedBase64CredentialIDs.map(id => ({ + id, + type: 'public-key', + transports: suggestedTransports, + })), + timeout, }; } diff --git a/packages/server/src/attestation/generateAttestationOptions.test.ts b/packages/server/src/attestation/generateAttestationOptions.test.ts index 3885dfc..73218bf 100644 --- a/packages/server/src/attestation/generateAttestationOptions.test.ts +++ b/packages/server/src/attestation/generateAttestationOptions.test.ts @@ -5,58 +5,54 @@ test('should generate credential request options suitable for sending via JSON', const rpID = 'not.real'; const challenge = 'totallyrandomvalue'; const userID = '1234'; - const username = 'usernameHere'; + const userName = 'usernameHere'; const timeout = 1; const attestationType = 'indirect'; - const options = generateAttestationOptions( + const options = generateAttestationOptions({ serviceName, rpID, challenge, userID, - username, + userName, timeout, attestationType, - ); + }); expect(options).toEqual({ - publicKey: { - challenge, - rp: { - name: serviceName, - id: rpID, - }, - user: { - id: userID, - name: username, - displayName: username, - }, - pubKeyCredParams: [ - { - alg: -7, - type: 'public-key', - }, - ], - timeout, - attestation: attestationType, - excludeCredentials: [], + challenge, + rp: { + name: serviceName, + id: rpID, + }, + user: { + id: userID, + name: userName, + displayName: userName, }, + pubKeyCredParams: [ + { + alg: -7, + type: 'public-key', + }, + ], + timeout, + attestation: attestationType, + excludeCredentials: [], }); }); test('should map excluded credential IDs if specified', () => { - const options = generateAttestationOptions( - 'SimpleWebAuthn', - 'not.real', - 'totallyrandomvalue', - '1234', - 'usernameHere', - undefined, - undefined, - ['someIDhere'], - ); + const options = generateAttestationOptions({ + serviceName: 'SimpleWebAuthn', + rpID: 'not.real', + challenge: 'totallyrandomvalue', + userID: '1234', + userName: 'usernameHere', + excludedBase64CredentialIDs: ['someIDhere'], + }); - expect(options.publicKey.excludeCredentials).toEqual([{ + expect(options.excludeCredentials).toEqual([{ id: 'someIDhere', type: 'public-key', transports: ['usb', 'ble', 'nfc', 'internal'], @@ -64,25 +60,25 @@ test('should map excluded credential IDs if specified', () => { }); test('defaults to 60 seconds if no timeout is specified', () => { - const options = generateAttestationOptions( - 'SimpleWebAuthn', - 'not.real', - 'totallyrandomvalue', - '1234', - 'usernameHere', - ); + const options = generateAttestationOptions({ + serviceName: 'SimpleWebAuthn', + rpID: 'not.real', + challenge: 'totallyrandomvalue', + userID: '1234', + userName: 'usernameHere', + }); - expect(options.publicKey.timeout).toEqual(60000); + expect(options.timeout).toEqual(60000); }); test('defaults to direct attestation if no attestation type is specified', () => { - const options = generateAttestationOptions( - 'SimpleWebAuthn', - 'not.real', - 'totallyrandomvalue', - '1234', - 'usernameHere', - ); + const options = generateAttestationOptions({ + serviceName: 'SimpleWebAuthn', + rpID: 'not.real', + challenge: 'totallyrandomvalue', + userID: '1234', + userName: 'usernameHere', + }); - expect(options.publicKey.attestation).toEqual('direct'); + expect(options.attestation).toEqual('none'); }); diff --git a/packages/server/src/attestation/generateAttestationOptions.ts b/packages/server/src/attestation/generateAttestationOptions.ts index 27931fa..e2a9926 100644 --- a/packages/server/src/attestation/generateAttestationOptions.ts +++ b/packages/server/src/attestation/generateAttestationOptions.ts @@ -1,57 +1,76 @@ -import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/typescript-types'; +import { + PublicKeyCredentialCreationOptionsJSON, +} from '@simplewebauthn/typescript-types'; + +type Options = { + serviceName: string, + rpID: string, + challenge: string, + userID: string, + userName: string, + userDisplayName?: string, + timeout?: number, + attestationType?: AttestationConveyancePreference, + excludedBase64CredentialIDs?: string[], + suggestedTransports?: AuthenticatorTransport[], +}; /** * Prepare a value to pass into navigator.credentials.create(...) for authenticator "registration" * + * **Options:** + * * @param serviceName Friendly user-visible website name * @param rpID Valid domain name (after `https://`) * @param challenge Random string the authenticator needs to sign and pass back * @param userID User's website-specific unique ID - * @param username User's website-specific username + * @param userName User's website-specific username (email, etc...) + * @param userDisplayName User's actual name * @param timeout How long (in ms) the user can take to complete attestation - * @param attestationType Request a full ("direct") or anonymized ("indirect") attestation statement + * @param attestationType Specific attestation statement * @param excludedBase64CredentialIDs Array of base64-encoded authenticator IDs registered by the * user so the user can't register the same credential multiple times * @param suggestedTransports Suggested types of authenticators for attestation */ export default function generateAttestationOptions( - serviceName: string, - rpID: string, - challenge: string, - userID: string, - username: string, - timeout = 60000, - attestationType: 'direct' | 'indirect' = 'direct', - excludedBase64CredentialIDs: string[] = [], - suggestedTransports: AuthenticatorTransport[] = ['usb', 'ble', 'nfc', 'internal'], + options: Options, ): PublicKeyCredentialCreationOptionsJSON { + const { + serviceName, + rpID, + challenge, + userID, + userName, + userDisplayName = userName, + timeout = 60000, + attestationType = 'none', + excludedBase64CredentialIDs = [], + suggestedTransports = ['usb', 'ble', 'nfc', 'internal'], + } = options; + return { - publicKey: { - // Cryptographically random bytes to prevent replay attacks - challenge, - // The organization registering and authenticating the user - rp: { - name: serviceName, - id: rpID, - }, - user: { - id: userID, - name: username, - displayName: username, - }, - pubKeyCredParams: [ - { - alg: -7, - type: 'public-key', - }, - ], - timeout, - attestation: attestationType, - excludeCredentials: excludedBase64CredentialIDs.map((id) => ({ - id, - type: 'public-key', - transports: suggestedTransports, - })), + challenge, + rp: { + name: serviceName, + id: rpID, + }, + user: { + id: userID, + name: userName, + displayName: userDisplayName, }, + pubKeyCredParams: [ + { + alg: -7, + type: 'public-key', + }, + ], + timeout, + attestation: attestationType, + excludeCredentials: excludedBase64CredentialIDs.map((id) => ({ + id, + type: 'public-key', + transports: suggestedTransports, + })), }; } diff --git a/packages/typescript-types/src/index.ts b/packages/typescript-types/src/index.ts index 353623e..920fd7c 100644 --- a/packages/typescript-types/src/index.ts +++ b/packages/typescript-types/src/index.ts @@ -1,56 +1,27 @@ /** * A variant of PublicKeyCredentialCreationOptions suitable for JSON transmission to the browser to * (eventually) get passed into navigator.credentials.create(...) in the browser. - * - * Noteworthy values: - * @param challenge A random string of characters. Will be converted to a Uint8Array in the browser - * @param user.id Your unique, internal ID for the user. Will be converted to a Uint8Array in the - * browser */ -export type PublicKeyCredentialCreationOptionsJSON = { - publicKey: { - challenge: string; - // The organization registering and authenticating the user - rp: { - name: string; - id: string; - }; - user: { - id: string; - name: string; - displayName: string; - }; - pubKeyCredParams: [ - { - alg: -7; - type: 'public-key'; - }, - ]; - timeout?: number; - attestation: 'direct' | 'indirect'; - excludeCredentials: PublicKeyCredentialDescriptorJSON[]; - }; -}; +export interface PublicKeyCredentialCreationOptionsJSON extends Omit< +PublicKeyCredentialCreationOptions, 'challenge' | 'user' | 'excludeCredentials' +> { + // Will be converted to a Uint8Array in the browser + user: PublicKeyCredentialUserEntityJSON; + challenge: string; + excludeCredentials: PublicKeyCredentialDescriptorJSON[]; +} /** * A variant of PublicKeyCredentialRequestOptions suitable for JSON transmission to the browser to * (eventually) get passed into navigator.credentials.get(...) in the browser. - * - * Noteworthy values: - * @param challenge A random string of characters. Will be converted to a Uint8Array in the browser - * @param allowCredentials.id Base64-encoded credentialId. Will be converted to a Uint8Array in the - * browser */ -export type PublicKeyCredentialRequestOptionsJSON = { - publicKey: { - challenge: string; - allowCredentials: PublicKeyCredentialDescriptorJSON[]; - // extensions?: AuthenticationExtensionsClientInputs, - rpId?: string; - timeout?: number; - userVerification?: UserVerificationRequirement; - }; -}; +export interface PublicKeyCredentialRequestOptionsJSON extends Omit< +PublicKeyCredentialRequestOptions, 'challenge' |'allowCredentials' +> { + // Will be converted to a Uint8Array in the browser + challenge: string; + allowCredentials: PublicKeyCredentialDescriptorJSON[]; +} export interface PublicKeyCredentialDescriptorJSON extends Omit< PublicKeyCredentialDescriptor, 'id' @@ -59,6 +30,13 @@ PublicKeyCredentialDescriptor, 'id' id: string; } +export interface PublicKeyCredentialUserEntityJSON extends Omit < +PublicKeyCredentialUserEntity, 'id' +> { + // Should be a Base64-encoded credential ID. Will be converted to a Uint8Array in the browser + id: string; +} + /** * The value returned from navigator.credentials.create() */ |