Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/reg-hints #653

Merged
merged 4 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
}
Loading