Skip to content

Commit

Permalink
Merge pull request #653 from MasterKale/feat/reg-hints
Browse files Browse the repository at this point in the history
feat/reg-hints
  • Loading branch information
MasterKale authored Dec 6, 2024
2 parents b99e441 + 3182bf4 commit ff45a3b
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 33 deletions.
8 changes: 8 additions & 0 deletions packages/browser/src/methods/startRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const goodOpts1: PublicKeyCredentialCreationOptionsJSON = {
transports: ['internal'],
},
],
hints: ['client-device', 'hybrid', 'security-key'],
attestationFormats: ['packed'],
};

/**
Expand Down Expand Up @@ -93,6 +95,12 @@ describe('Method: startRegistration', () => {
assertEquals(credId.byteLength, 64);
assertEquals(argsPublicKey.excludeCredentials?.[0].type, 'public-key');
assertEquals(argsPublicKey.excludeCredentials?.[0].transports, ['internal']);

// Confirm hints and attestationFormats
// @ts-ignore: we know `hints` are becoming available in browsers
assertEquals(argsPublicKey.hints, ['client-device', 'hybrid', 'security-key']);
// @ts-ignore: we know `attestationFormats` are becoming available in browsers
assertEquals(argsPublicKey.attestationFormats, ['packed']);
});

it('should return base64url-encoded response values', async () => {
Expand Down
25 changes: 12 additions & 13 deletions packages/server/src/authentication/generateAuthenticationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,12 @@ import type {
AuthenticatorTransportFuture,
Base64URLString,
PublicKeyCredentialRequestOptionsJSON,
UserVerificationRequirement,
} from '@simplewebauthn/types';

import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.ts';
import { generateChallenge } from '../helpers/generateChallenge.ts';

export type GenerateAuthenticationOptionsOpts = {
rpID: string;
allowCredentials?: {
id: Base64URLString;
transports?: AuthenticatorTransportFuture[];
}[];
challenge?: string | Uint8Array;
timeout?: number;
userVerification?: UserVerificationRequirement;
extensions?: AuthenticationExtensionsClientInputs;
};
export type GenerateAuthenticationOptionsOpts = Parameters<typeof generateAuthenticationOptions>[0];

/**
* Prepare a value to pass into navigator.credentials.get(...) for authenticator authentication
Expand All @@ -34,7 +23,17 @@ export type GenerateAuthenticationOptionsOpts = {
* @param extensions **(Optional)** - Additional plugins the authenticator or browser should use during authentication
*/
export async function generateAuthenticationOptions(
options: GenerateAuthenticationOptionsOpts,
options: {
rpID: string;
allowCredentials?: {
id: Base64URLString;
transports?: AuthenticatorTransportFuture[];
}[];
challenge?: string | Uint8Array;
timeout?: number;
userVerification?: 'required' | 'preferred' | 'discouraged';
extensions?: AuthenticationExtensionsClientInputs;
},
): Promise<PublicKeyCredentialRequestOptionsJSON> {
const {
allowCredentials,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Deno.test('should generate credential request options suitable for sending via J
const userID = isoUint8Array.fromUTF8String('1234');
const userName = 'usernameHere';
const timeout = 1;
const attestationType = 'indirect';
const attestationType = 'direct';
const userDisplayName = 'userDisplayName';

const options = await generateRegistrationOptions({
Expand Down Expand Up @@ -56,6 +56,7 @@ Deno.test('should generate credential request options suitable for sending via J
extensions: {
credProps: true,
},
hints: [],
},
);
});
Expand Down Expand Up @@ -334,3 +335,54 @@ Deno.test('should raise if string is specified for userID', async () => {
'String values for `userID` are no longer supported. See https://simplewebauthn.dev/docs/advanced/server/custom-user-ids',
);
});

Deno.test('should map undefined authenticator preference to empty hint', async () => {
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
userName: 'usernameHere',
preferredAuthenticatorType: undefined,
});

assertEquals(options.hints, []);
});

Deno.test('should map "securityKey" authenticator preference to hint and attachment', async () => {
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
userName: 'usernameHere',
preferredAuthenticatorType: 'securityKey',
});

assertEquals(options.hints, ['security-key']);
assertEquals(options.authenticatorSelection?.authenticatorAttachment, 'cross-platform');
});

Deno.test('should map "localDevice" authenticator preference to hint and attachment', async () => {
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
userName: 'usernameHere',
preferredAuthenticatorType: 'localDevice',
});

assertEquals(options.hints, ['client-device']);
assertEquals(options.authenticatorSelection?.authenticatorAttachment, 'platform');
});

Deno.test('should map "remoteDevice" authenticator preference to hint and attachment', async () => {
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
userName: 'usernameHere',
preferredAuthenticatorType: 'remoteDevice',
});

assertEquals(options.hints, ['hybrid']);
assertEquals(options.authenticatorSelection?.authenticatorAttachment, 'cross-platform');
});
60 changes: 41 additions & 19 deletions packages/server/src/registration/generateRegistrationOptions.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
import type {
AttestationConveyancePreference,
AuthenticationExtensionsClientInputs,
AuthenticatorSelectionCriteria,
AuthenticatorTransportFuture,
Base64URLString,
COSEAlgorithmIdentifier,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialHint,
PublicKeyCredentialParameters,
} from '@simplewebauthn/types';

import { generateChallenge } from '../helpers/generateChallenge.ts';
import { generateUserID } from '../helpers/generateUserID.ts';
import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.ts';

export type GenerateRegistrationOptionsOpts = {
rpName: string;
rpID: string;
userName: string;
userID?: Uint8Array;
challenge?: string | Uint8Array;
userDisplayName?: string;
timeout?: number;
attestationType?: AttestationConveyancePreference;
excludeCredentials?: {
id: Base64URLString;
transports?: AuthenticatorTransportFuture[];
}[];
authenticatorSelection?: AuthenticatorSelectionCriteria;
extensions?: AuthenticationExtensionsClientInputs;
supportedAlgorithmIDs?: COSEAlgorithmIdentifier[];
};
export type GenerateRegistrationOptionsOpts = Parameters<typeof generateRegistrationOptions>[0];

/**
* Supported crypto algo identifiers
Expand Down Expand Up @@ -96,9 +80,27 @@ const defaultSupportedAlgorithmIDs: COSEAlgorithmIdentifier[] = [-8, -7, -257];
* @param authenticatorSelection **(Optional)** - Advanced criteria for restricting the types of authenticators that may be used. Defaults to `{ residentKey: 'preferred', userVerification: 'preferred' }`
* @param extensions **(Optional)** - Additional plugins the authenticator or browser should use during attestation
* @param supportedAlgorithmIDs **(Optional)** - Array of numeric COSE algorithm identifiers supported for attestation by this RP. See https://www.iana.org/assignments/cose/cose.xhtml#algorithms. Defaults to `[-8, -7, -257]`
* @param preferredAuthenticatorType **(Optional)** - Encourage the browser to prompt the user to register a specific type of authenticator
*/
export async function generateRegistrationOptions(
options: GenerateRegistrationOptionsOpts,
options: {
rpName: string;
rpID: string;
userName: string;
userID?: Uint8Array;
challenge?: string | Uint8Array;
userDisplayName?: string;
timeout?: number;
attestationType?: 'direct' | 'enterprise' | 'none';
excludeCredentials?: {
id: Base64URLString;
transports?: AuthenticatorTransportFuture[];
}[];
authenticatorSelection?: AuthenticatorSelectionCriteria;
extensions?: AuthenticationExtensionsClientInputs;
supportedAlgorithmIDs?: COSEAlgorithmIdentifier[];
preferredAuthenticatorType?: 'securityKey' | 'localDevice' | 'remoteDevice';
},
): Promise<PublicKeyCredentialCreationOptionsJSON> {
const {
rpName,
Expand All @@ -113,6 +115,7 @@ export async function generateRegistrationOptions(
authenticatorSelection = defaultAuthenticatorSelection,
extensions,
supportedAlgorithmIDs = defaultSupportedAlgorithmIDs,
preferredAuthenticatorType,
} = options;

/**
Expand Down Expand Up @@ -181,6 +184,24 @@ export async function generateRegistrationOptions(
_userID = await generateUserID();
}

/**
* Map authenticator preference to hints. Map to authenticatorAttachment as well for
* backwards-compatibility.
*/
const hints: PublicKeyCredentialHint[] = [];
if (preferredAuthenticatorType) {
if (preferredAuthenticatorType === 'securityKey') {
hints.push('security-key');
authenticatorSelection.authenticatorAttachment = 'cross-platform';
} else if (preferredAuthenticatorType === 'localDevice') {
hints.push('client-device');
authenticatorSelection.authenticatorAttachment = 'platform';
} else if (preferredAuthenticatorType === 'remoteDevice') {
hints.push('hybrid');
authenticatorSelection.authenticatorAttachment = 'cross-platform';
}
}

return {
challenge: isoBase64URL.fromBuffer(_challenge),
rp: {
Expand Down Expand Up @@ -211,5 +232,6 @@ export async function generateRegistrationOptions(
...extensions,
credProps: true,
},
hints,
};
}

0 comments on commit ff45a3b

Please sign in to comment.