From 8e08190620bdd256e566c214d70354193ac6dd2e Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 28 Apr 2023 10:58:26 +0200 Subject: [PATCH] [EC-598] feat: only show fallback options if supported --- apps/browser/src/browser/webauthn-utils.ts | 8 +++-- apps/browser/src/fido2/content/page-script.ts | 24 ++++++++++++--- .../fido2/popup/fido2/fido2.component.html | 7 ++++- .../src/fido2/popup/fido2/fido2.component.ts | 6 ++-- .../browser-fido2-user-interface.service.ts | 29 +++++++++++++++++-- ...fido2-authenticator.service.abstraction.ts | 4 +++ .../fido2-client.service.abstraction.ts | 2 ++ ...ido2-user-interface.service.abstraction.ts | 5 +++- .../services/fido2-authenticator.service.ts | 10 +++++-- .../fido2/services/fido2-client.service.ts | 2 ++ 10 files changed, 81 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/browser/webauthn-utils.ts b/apps/browser/src/browser/webauthn-utils.ts index 7778b545163..b59fd46432d 100644 --- a/apps/browser/src/browser/webauthn-utils.ts +++ b/apps/browser/src/browser/webauthn-utils.ts @@ -10,7 +10,8 @@ export class WebauthnUtils { static mapCredentialCreationOptions( options: CredentialCreationOptions, origin: string, - sameOriginWithAncestors: boolean + sameOriginWithAncestors: boolean, + fallbackSupported: boolean ): CreateCredentialParams { const keyOptions = options.publicKey; @@ -47,6 +48,7 @@ export class WebauthnUtils { }, timeout: keyOptions.timeout, sameOriginWithAncestors, + fallbackSupported, }; } @@ -91,7 +93,8 @@ export class WebauthnUtils { static mapCredentialRequestOptions( options: CredentialRequestOptions, origin: string, - sameOriginWithAncestors: boolean + sameOriginWithAncestors: boolean, + fallbackSupported: boolean ): AssertCredentialParams { const keyOptions = options.publicKey; @@ -108,6 +111,7 @@ export class WebauthnUtils { userVerification: keyOptions.userVerification, timeout: keyOptions.timeout, sameOriginWithAncestors, + fallbackSupported, }; } diff --git a/apps/browser/src/fido2/content/page-script.ts b/apps/browser/src/fido2/content/page-script.ts index 8f8f48e179e..e72b2f24441 100644 --- a/apps/browser/src/fido2/content/page-script.ts +++ b/apps/browser/src/fido2/content/page-script.ts @@ -55,12 +55,22 @@ navigator.credentials.create = async ( options?: CredentialCreationOptions, abortController?: AbortController ): Promise => { + const fallbackSupported = + (options?.publicKey?.authenticatorSelection.authenticatorAttachment === "platform" && + browserNativeWebauthnPlatformAuthenticatorSupport) || + (options?.publicKey?.authenticatorSelection.authenticatorAttachment !== "platform" && + browserNativeWebauthnSupport); try { const response = await messenger.request( { type: MessageType.CredentialCreationRequest, // TODO: Fix sameOriginWithAncestors! - data: WebauthnUtils.mapCredentialCreationOptions(options, window.location.origin, true), + data: WebauthnUtils.mapCredentialCreationOptions( + options, + window.location.origin, + true, + fallbackSupported + ), }, abortController ); @@ -71,7 +81,7 @@ navigator.credentials.create = async ( return WebauthnUtils.mapCredentialRegistrationResult(response.result); } catch (error) { - if (error && error.fallbackRequested) { + if (error && error.fallbackRequested && fallbackSupported) { return await browserCredentials.create(options); } @@ -83,12 +93,18 @@ navigator.credentials.get = async ( options?: CredentialRequestOptions, abortController?: AbortController ): Promise => { + const fallbackSupported = browserNativeWebauthnSupport; try { const response = await messenger.request( { type: MessageType.CredentialGetRequest, // TODO: Fix sameOriginWithAncestors! - data: WebauthnUtils.mapCredentialRequestOptions(options, window.location.origin, true), + data: WebauthnUtils.mapCredentialRequestOptions( + options, + window.location.origin, + true, + fallbackSupported + ), }, abortController ); @@ -99,7 +115,7 @@ navigator.credentials.get = async ( return WebauthnUtils.mapCredentialAssertResult(response.result); } catch (error) { - if (error && error.fallbackRequested) { + if (error && error.fallbackRequested && fallbackSupported) { return await browserCredentials.get(options); } diff --git a/apps/browser/src/fido2/popup/fido2/fido2.component.html b/apps/browser/src/fido2/popup/fido2/fido2.component.html index 6afb62801c2..9499d4febb2 100644 --- a/apps/browser/src/fido2/popup/fido2/fido2.component.html +++ b/apps/browser/src/fido2/popup/fido2/fido2.component.html @@ -45,7 +45,12 @@ You do not have a matching login for this site. - diff --git a/apps/browser/src/fido2/popup/fido2/fido2.component.ts b/apps/browser/src/fido2/popup/fido2/fido2.component.ts index b9ada0d9b58..88892ce78a3 100644 --- a/apps/browser/src/fido2/popup/fido2/fido2.component.ts +++ b/apps/browser/src/fido2/popup/fido2/fido2.component.ts @@ -27,6 +27,7 @@ import { interface ViewData { message: BrowserFido2Message; showUnsupportedVerification: boolean; + fallbackSupported: boolean; } @Component({ @@ -108,11 +109,10 @@ export class Fido2Component implements OnInit, OnDestroy { return { message, showUnsupportedVerification: - (message.type === "ConfirmNewCredentialRequest" || - message.type === "ConfirmNewNonDiscoverableCredentialRequest" || - message.type === "PickCredentialRequest") && + "userVerification" in message && message.userVerification && !(await this.passwordRepromptService.enabled()), + fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, }; }), takeUntil(this.destroy$) diff --git a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts index 90d0d3a358e..aafaa0d437e 100644 --- a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts @@ -48,6 +48,7 @@ export type BrowserFido2Message = { sessionId: string } & ( type: "PickCredentialRequest"; cipherIds: string[]; userVerification: boolean; + fallbackSupported: boolean; } | { type: "PickCredentialResponse"; @@ -59,6 +60,7 @@ export type BrowserFido2Message = { sessionId: string } & ( credentialName: string; userName: string; userVerification: boolean; + fallbackSupported: boolean; } | { type: "ConfirmNewCredentialResponse"; @@ -69,6 +71,7 @@ export type BrowserFido2Message = { sessionId: string } & ( credentialName: string; userName: string; userVerification: boolean; + fallbackSupported: boolean; } | { type: "ConfirmNewNonDiscoverableCredentialResponse"; @@ -78,9 +81,11 @@ export type BrowserFido2Message = { sessionId: string } & ( | { type: "InformExcludedCredentialRequest"; existingCipherIds: string[]; + fallbackSupported: boolean; } | { type: "InformCredentialNotFoundRequest"; + fallbackSupported: boolean; } | { type: "AbortRequest"; @@ -94,17 +99,29 @@ export type BrowserFido2Message = { sessionId: string } & ( export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { constructor(private popupUtilsService: PopupUtilsService) {} - async newSession(abortController?: AbortController): Promise { - return await BrowserFido2UserInterfaceSession.create(this.popupUtilsService, abortController); + async newSession( + fallbackSupported: boolean, + abortController?: AbortController + ): Promise { + return await BrowserFido2UserInterfaceSession.create( + this.popupUtilsService, + fallbackSupported, + abortController + ); } } export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession { static async create( popupUtilsService: PopupUtilsService, + fallbackSupported: boolean, abortController?: AbortController ): Promise { - return new BrowserFido2UserInterfaceSession(popupUtilsService, abortController); + return new BrowserFido2UserInterfaceSession( + popupUtilsService, + fallbackSupported, + abortController + ); } static sendMessage(msg: BrowserFido2Message) { @@ -121,6 +138,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi private constructor( private readonly popupUtilsService: PopupUtilsService, + private readonly fallbackSupported: boolean, readonly abortController = new AbortController(), readonly sessionId = Utils.newGuid() ) { @@ -182,6 +200,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi cipherIds, sessionId: this.sessionId, userVerification, + fallbackSupported: this.fallbackSupported, }; await this.send(data); @@ -201,6 +220,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi credentialName, userName, userVerification, + fallbackSupported: this.fallbackSupported, }; await this.send(data); @@ -220,6 +240,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi credentialName, userName, userVerification, + fallbackSupported: this.fallbackSupported, }; await this.send(data); @@ -233,6 +254,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi type: "InformExcludedCredentialRequest", sessionId: this.sessionId, existingCipherIds, + fallbackSupported: this.fallbackSupported, }; await this.send(data); @@ -243,6 +265,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi const data: BrowserFido2Message = { type: "InformCredentialNotFoundRequest", sessionId: this.sessionId, + fallbackSupported: this.fallbackSupported, }; await this.send(data); diff --git a/libs/common/src/fido2/abstractions/fido2-authenticator.service.abstraction.ts b/libs/common/src/fido2/abstractions/fido2-authenticator.service.abstraction.ts index 74fc073a058..3e842700bc3 100644 --- a/libs/common/src/fido2/abstractions/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/fido2/abstractions/fido2-authenticator.service.abstraction.ts @@ -84,6 +84,8 @@ export interface Fido2AuthenticatorMakeCredentialsParams { /** The effective resident key requirement for credential creation, a Boolean value determined by the client. */ requireResidentKey: boolean; requireUserVerification: boolean; + /** Forwarded to user interface */ + fallbackSupported: boolean; /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ // requireUserPresence: true; // Always required } @@ -106,6 +108,8 @@ export interface Fido2AuthenticatorGetAssertionParams { /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ // requireUserPresence: boolean; // Always required extensions: unknown; + /** Forwarded to user interface */ + fallbackSupported: boolean; } export interface Fido2AuthenticatorGetAssertionResult { diff --git a/libs/common/src/fido2/abstractions/fido2-client.service.abstraction.ts b/libs/common/src/fido2/abstractions/fido2-client.service.abstraction.ts index 61fba18364f..baefc6d509d 100644 --- a/libs/common/src/fido2/abstractions/fido2-client.service.abstraction.ts +++ b/libs/common/src/fido2/abstractions/fido2-client.service.abstraction.ts @@ -44,6 +44,7 @@ export interface CreateCredentialParams { id: string; // b64 encoded displayName: string; }; + fallbackSupported: boolean; timeout?: number; } @@ -64,6 +65,7 @@ export interface AssertCredentialParams { userVerification?: UserVerification; timeout: number; sameOriginWithAncestors: boolean; + fallbackSupported: boolean; } export interface AssertCredentialResult { diff --git a/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts b/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts index 11ab6439aa5..3d0db42d3c1 100644 --- a/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts @@ -10,7 +10,10 @@ export interface PickCredentialParams { } export abstract class Fido2UserInterfaceService { - newSession: (abortController?: AbortController) => Promise; + newSession: ( + fallbackSupported: boolean, + abortController?: AbortController + ) => Promise; } export abstract class Fido2UserInterfaceSession { diff --git a/libs/common/src/fido2/services/fido2-authenticator.service.ts b/libs/common/src/fido2/services/fido2-authenticator.service.ts index d5a0a349493..2526314250d 100644 --- a/libs/common/src/fido2/services/fido2-authenticator.service.ts +++ b/libs/common/src/fido2/services/fido2-authenticator.service.ts @@ -43,7 +43,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr params: Fido2AuthenticatorMakeCredentialsParams, abortController?: AbortController ): Promise { - const userInterfaceSession = await this.userInterface.newSession(abortController); + const userInterfaceSession = await this.userInterface.newSession( + params.fallbackSupported, + abortController + ); try { if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { @@ -211,7 +214,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr params: Fido2AuthenticatorGetAssertionParams, abortController?: AbortController ): Promise { - const userInterfaceSession = await this.userInterface.newSession(abortController); + const userInterfaceSession = await this.userInterface.newSession( + params.fallbackSupported, + abortController + ); try { if ( diff --git a/libs/common/src/fido2/services/fido2-client.service.ts b/libs/common/src/fido2/services/fido2-client.service.ts index 0023b6f9d95..084a8b03cca 100644 --- a/libs/common/src/fido2/services/fido2-client.service.ts +++ b/libs/common/src/fido2/services/fido2-client.service.ts @@ -127,6 +127,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { id: Fido2Utils.stringToBuffer(params.user.id), displayName: params.user.displayName, }, + fallbackSupported: params.fallbackSupported, }; let makeCredentialResult; try { @@ -226,6 +227,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { hash: clientDataHash, allowCredentialDescriptorList, extensions: {}, + fallbackSupported: params.fallbackSupported, }; let getAssertionResult;