From a8e09094f8afdb016f437a3bc3bb6c6586330840 Mon Sep 17 00:00:00 2001 From: Itai Hanski Date: Sun, 29 Sep 2024 09:09:16 +0300 Subject: [PATCH] feat: Add passkeyOptions to all webauthn start calls (#807) RELEASE ## Related Issues Fixes https://github.com/descope/etc/issues/7847 ## Description Add optional passkeyOptions to all WebAuthn start calls ## Must - [x] Tests - [x] Documentation (if applicable) --- packages/sdks/core-js-sdk/src/index.ts | 1 + packages/sdks/core-js-sdk/src/sdk/types.ts | 18 +++++++++ packages/sdks/core-js-sdk/src/sdk/webauthn.ts | 11 +++++- .../core-js-sdk/test/sdk/webauthn.test.ts | 39 ++++++++++++++++++- packages/sdks/web-js-sdk/src/sdk/webauthn.ts | 24 +++++++++--- packages/sdks/web-js-sdk/src/types.ts | 2 +- 6 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/sdks/core-js-sdk/src/index.ts b/packages/sdks/core-js-sdk/src/index.ts index 93dc55b0f..6ac1a2131 100644 --- a/packages/sdks/core-js-sdk/src/index.ts +++ b/packages/sdks/core-js-sdk/src/index.ts @@ -41,6 +41,7 @@ export type { UserHistoryResponse, LoginOptions, AccessKeyLoginOptions, + PasskeyOptions, } from './sdk/types'; export * from './utils'; export { default as HttpStatusCodes } from './constants/httpStatusCodes'; diff --git a/packages/sdks/core-js-sdk/src/sdk/types.ts b/packages/sdks/core-js-sdk/src/sdk/types.ts index 1fb1d5efa..81c1a8758 100644 --- a/packages/sdks/core-js-sdk/src/sdk/types.ts +++ b/packages/sdks/core-js-sdk/src/sdk/types.ts @@ -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; diff --git a/packages/sdks/core-js-sdk/src/sdk/webauthn.ts b/packages/sdks/core-js-sdk/src/sdk/webauthn.ts index 624356527..d15aaac86 100644 --- a/packages/sdks/core-js-sdk/src/sdk/webauthn.ts +++ b/packages/sdks/core-js-sdk/src/sdk/webauthn.ts @@ -6,6 +6,7 @@ import { ResponseData, LoginOptions, JWTResponse, + PasskeyOptions, WebAuthnStartResponse, } from './types'; import { string, stringNonEmpty, withValidations } from './validations'; @@ -44,6 +45,7 @@ const withWebauthn = (httpClient: HttpClient) => ({ loginId: string, origin: string, name: string, + passkeyOptions?: PasskeyOptions, ): Promise> => transformResponse( httpClient.post(apiPaths.webauthn.signUp.start, { @@ -52,6 +54,7 @@ const withWebauthn = (httpClient: HttpClient) => ({ name, }, origin, + passkeyOptions, }), ), ), @@ -77,11 +80,12 @@ const withWebauthn = (httpClient: HttpClient) => ({ origin: string, loginOptions?: LoginOptions, token?: string, + passkeyOptions?: PasskeyOptions, ): Promise> => transformResponse( httpClient.post( apiPaths.webauthn.signIn.start, - { loginId, origin, loginOptions }, + { loginId, origin, loginOptions, passkeyOptions }, { token }, ), ), @@ -106,11 +110,13 @@ const withWebauthn = (httpClient: HttpClient) => ({ ( loginId: string, origin: string, + passkeyOptions?: PasskeyOptions, ): Promise> => transformResponse( httpClient.post(apiPaths.webauthn.signUpOrIn.start, { loginId, origin, + passkeyOptions, }), ), ), @@ -122,11 +128,12 @@ const withWebauthn = (httpClient: HttpClient) => ({ loginId: string, origin: string, token: string, + passkeyOptions?: PasskeyOptions, ): Promise> => transformResponse( httpClient.post( apiPaths.webauthn.update.start, - { loginId, origin }, + { loginId, origin, passkeyOptions }, { token }, ), ), diff --git a/packages/sdks/core-js-sdk/test/sdk/webauthn.test.ts b/packages/sdks/core-js-sdk/test/sdk/webauthn.test.ts index 588899cc8..20a9cee25 100644 --- a/packages/sdks/core-js-sdk/test/sdk/webauthn.test.ts +++ b/packages/sdks/core-js-sdk/test/sdk/webauthn.test.ts @@ -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', + }, + }, }, ); }); @@ -201,7 +220,17 @@ 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, @@ -209,6 +238,11 @@ describe('webauthn', () => { loginId: 'loginId', origin: 'origin', loginOptions: undefined, + token: undefined, + passkeyOptions: { + userVerification: 'required', + extensionsJSON: '{}', + }, }, { token: undefined }, ); @@ -387,6 +421,7 @@ describe('webauthn', () => { { loginId: 'loginId', origin: 'origin', + passkeyOptions: undefined, }, ); }); diff --git a/packages/sdks/web-js-sdk/src/sdk/webauthn.ts b/packages/sdks/web-js-sdk/src/sdk/webauthn.ts index 649ac0768..340f98de6 100644 --- a/packages/sdks/web-js-sdk/src/sdk/webauthn.ts +++ b/packages/sdks/web-js-sdk/src/sdk/webauthn.ts @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/packages/sdks/web-js-sdk/src/types.ts b/packages/sdks/web-js-sdk/src/types.ts index bce0194ad..b6b38d49a 100644 --- a/packages/sdks/web-js-sdk/src/types.ts +++ b/packages/sdks/web-js-sdk/src/types.ts @@ -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';