Skip to content

Commit

Permalink
feat: Add passkeyOptions to all webauthn start calls (#807) RELEASE
Browse files Browse the repository at this point in the history
## Related Issues

Fixes descope/etc#7847

## Description

Add optional passkeyOptions to all WebAuthn start calls

## Must

- [x] Tests
- [x] Documentation (if applicable)
  • Loading branch information
itaihanski authored Sep 29, 2024
1 parent 3232f34 commit a8e0909
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/sdks/core-js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type {
UserHistoryResponse,
LoginOptions,
AccessKeyLoginOptions,
PasskeyOptions,
} from './sdk/types';
export * from './utils';
export { default as HttpStatusCodes } from './constants/httpStatusCodes';
Expand Down
18 changes: 18 additions & 0 deletions packages/sdks/core-js-sdk/src/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ export type ExchangeAccessKeyResponse = {
expiration: number;
};

/** Options for fine-grained passkey (WebAuthn) control */
export type PasskeyOptions = {
// attestation only (sign up)
authenticatorSelection?: WebauthnAuthenticatorSelectionCriteria;
attestation?: 'none' | 'indirect' | 'direct';
// assertion only (sign in)
userVerification?: 'preferred' | 'required' | 'discouraged';
// shared
extensionsJSON?: string;
};

/** Part of the passkey options that apply when performing attestation (sign up) */
export type WebauthnAuthenticatorSelectionCriteria = {
authenticatorAttachment?: 'any' | 'platform' | 'crossplatform';
residentKey?: 'discouraged' | 'preferred' | 'required';
userVerification?: 'preferred' | 'required' | 'discouraged';
};

/** The response returned from the various start webauthn functions */
export type WebAuthnStartResponse = {
transactionId: string;
Expand Down
11 changes: 9 additions & 2 deletions packages/sdks/core-js-sdk/src/sdk/webauthn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ResponseData,
LoginOptions,
JWTResponse,
PasskeyOptions,
WebAuthnStartResponse,
} from './types';
import { string, stringNonEmpty, withValidations } from './validations';
Expand Down Expand Up @@ -44,6 +45,7 @@ const withWebauthn = (httpClient: HttpClient) => ({
loginId: string,
origin: string,
name: string,
passkeyOptions?: PasskeyOptions,
): Promise<SdkResponse<WebAuthnStartResponse>> =>
transformResponse(
httpClient.post(apiPaths.webauthn.signUp.start, {
Expand All @@ -52,6 +54,7 @@ const withWebauthn = (httpClient: HttpClient) => ({
name,
},
origin,
passkeyOptions,
}),
),
),
Expand All @@ -77,11 +80,12 @@ const withWebauthn = (httpClient: HttpClient) => ({
origin: string,
loginOptions?: LoginOptions,
token?: string,
passkeyOptions?: PasskeyOptions,
): Promise<SdkResponse<WebAuthnStartResponse>> =>
transformResponse(
httpClient.post(
apiPaths.webauthn.signIn.start,
{ loginId, origin, loginOptions },
{ loginId, origin, loginOptions, passkeyOptions },
{ token },
),
),
Expand All @@ -106,11 +110,13 @@ const withWebauthn = (httpClient: HttpClient) => ({
(
loginId: string,
origin: string,
passkeyOptions?: PasskeyOptions,
): Promise<SdkResponse<WebAuthnStartResponse>> =>
transformResponse(
httpClient.post(apiPaths.webauthn.signUpOrIn.start, {
loginId,
origin,
passkeyOptions,
}),
),
),
Expand All @@ -122,11 +128,12 @@ const withWebauthn = (httpClient: HttpClient) => ({
loginId: string,
origin: string,
token: string,
passkeyOptions?: PasskeyOptions,
): Promise<SdkResponse<WebAuthnStartResponse>> =>
transformResponse(
httpClient.post(
apiPaths.webauthn.update.start,
{ loginId, origin },
{ loginId, origin, passkeyOptions },
{ token },
),
),
Expand Down
39 changes: 37 additions & 2 deletions packages/sdks/core-js-sdk/test/sdk/webauthn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,32 @@ describe('webauthn', () => {
};
mockHttpClient.post.mockResolvedValue(httpResponse);

sdk.webauthn.signUp.start('loginId', 'origin', 'John Doe');
const passkeyOptions = {
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'required',
},
};
sdk.webauthn.signUp.start(
'loginId',
'origin',
'John Doe',
passkeyOptions,
);

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.webauthn.signUp.start,
{
user: { loginId: 'loginId', name: 'John Doe' },
origin: 'origin',
passkeyOptions: {
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'required',
},
},
},
);
});
Expand Down Expand Up @@ -201,14 +220,29 @@ describe('webauthn', () => {
};
mockHttpClient.post.mockResolvedValue(httpResponse);

sdk.webauthn.signIn.start('loginId', 'origin');
const passkeyOptions = {
userVerification: 'required',
extensionsJSON: '{}',
};
sdk.webauthn.signIn.start(
'loginId',
'origin',
undefined,
undefined,
passkeyOptions,
);

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.webauthn.signIn.start,
{
loginId: 'loginId',
origin: 'origin',
loginOptions: undefined,
token: undefined,
passkeyOptions: {
userVerification: 'required',
extensionsJSON: '{}',
},
},
{ token: undefined },
);
Expand Down Expand Up @@ -387,6 +421,7 @@ describe('webauthn', () => {
{
loginId: 'loginId',
origin: 'origin',
passkeyOptions: undefined,
},
);
});
Expand Down
24 changes: 19 additions & 5 deletions packages/sdks/web-js-sdk/src/sdk/webauthn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JWTResponse, SdkResponse, ResponseData } from '@descope/core-js-sdk';
import { IS_BROWSER } from '../constants';
import { CoreSdk } from '../types';
import { CoreSdk, PasskeyOptions } from '../types';

type CreateWebauthn = typeof createWebAuthn;

Expand All @@ -25,11 +25,16 @@ const withCoreFns =

/** Constructs a higher level WebAuthn API that wraps the functions from code-js-sdk */
const createWebAuthn = (sdk: CoreSdk) => ({
async signUp(identifier: string, name: string) {
async signUp(
identifier: string,
name: string,
passkeyOptions?: PasskeyOptions,
) {
const startResponse = await sdk.webauthn.signUp.start(
identifier,
window.location.origin,
name,
passkeyOptions,
);
if (!startResponse.ok) {
return startResponse as unknown as SdkResponse<JWTResponse>;
Expand All @@ -42,10 +47,13 @@ const createWebAuthn = (sdk: CoreSdk) => ({
return finishResponse;
},

async signIn(identifier: string) {
async signIn(identifier: string, passkeyOptions?: PasskeyOptions) {
const startResponse = await sdk.webauthn.signIn.start(
identifier,
window.location.origin,
undefined,
undefined,
passkeyOptions,
);
if (!startResponse.ok) {
return startResponse as unknown as SdkResponse<JWTResponse>;
Expand All @@ -58,10 +66,11 @@ const createWebAuthn = (sdk: CoreSdk) => ({
return finishResponse;
},

async signUpOrIn(identifier: string) {
async signUpOrIn(identifier: string, passkeyOptions?: PasskeyOptions) {
const startResponse = await sdk.webauthn.signUpOrIn.start(
identifier,
window.location.origin,
passkeyOptions,
);
if (!startResponse.ok) {
return startResponse as unknown as SdkResponse<JWTResponse>;
Expand All @@ -83,11 +92,16 @@ const createWebAuthn = (sdk: CoreSdk) => ({
}
},

async update(identifier: string, token: string) {
async update(
identifier: string,
token: string,
passkeyOptions?: PasskeyOptions,
) {
const startResponse = await sdk.webauthn.update.start(
identifier,
window.location.origin,
token,
passkeyOptions,
);
if (!startResponse.ok) {
return startResponse as SdkResponse<ResponseData>;
Expand Down
2 changes: 1 addition & 1 deletion packages/sdks/web-js-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ export type AfterRequestHook = Extract<
Function
>;

export type { UserResponse } from '@descope/core-js-sdk';
export type { UserResponse, PasskeyOptions } from '@descope/core-js-sdk';

0 comments on commit a8e0909

Please sign in to comment.