1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[EC-598] feat: only show fallback options if supported

This commit is contained in:
Andreas Coroiu
2023-04-28 10:58:26 +02:00
parent c3ce8d87f0
commit 8e08190620
10 changed files with 81 additions and 16 deletions

View File

@@ -10,7 +10,8 @@ export class WebauthnUtils {
static mapCredentialCreationOptions( static mapCredentialCreationOptions(
options: CredentialCreationOptions, options: CredentialCreationOptions,
origin: string, origin: string,
sameOriginWithAncestors: boolean sameOriginWithAncestors: boolean,
fallbackSupported: boolean
): CreateCredentialParams { ): CreateCredentialParams {
const keyOptions = options.publicKey; const keyOptions = options.publicKey;
@@ -47,6 +48,7 @@ export class WebauthnUtils {
}, },
timeout: keyOptions.timeout, timeout: keyOptions.timeout,
sameOriginWithAncestors, sameOriginWithAncestors,
fallbackSupported,
}; };
} }
@@ -91,7 +93,8 @@ export class WebauthnUtils {
static mapCredentialRequestOptions( static mapCredentialRequestOptions(
options: CredentialRequestOptions, options: CredentialRequestOptions,
origin: string, origin: string,
sameOriginWithAncestors: boolean sameOriginWithAncestors: boolean,
fallbackSupported: boolean
): AssertCredentialParams { ): AssertCredentialParams {
const keyOptions = options.publicKey; const keyOptions = options.publicKey;
@@ -108,6 +111,7 @@ export class WebauthnUtils {
userVerification: keyOptions.userVerification, userVerification: keyOptions.userVerification,
timeout: keyOptions.timeout, timeout: keyOptions.timeout,
sameOriginWithAncestors, sameOriginWithAncestors,
fallbackSupported,
}; };
} }

View File

@@ -55,12 +55,22 @@ navigator.credentials.create = async (
options?: CredentialCreationOptions, options?: CredentialCreationOptions,
abortController?: AbortController abortController?: AbortController
): Promise<Credential> => { ): Promise<Credential> => {
const fallbackSupported =
(options?.publicKey?.authenticatorSelection.authenticatorAttachment === "platform" &&
browserNativeWebauthnPlatformAuthenticatorSupport) ||
(options?.publicKey?.authenticatorSelection.authenticatorAttachment !== "platform" &&
browserNativeWebauthnSupport);
try { try {
const response = await messenger.request( const response = await messenger.request(
{ {
type: MessageType.CredentialCreationRequest, type: MessageType.CredentialCreationRequest,
// TODO: Fix sameOriginWithAncestors! // TODO: Fix sameOriginWithAncestors!
data: WebauthnUtils.mapCredentialCreationOptions(options, window.location.origin, true), data: WebauthnUtils.mapCredentialCreationOptions(
options,
window.location.origin,
true,
fallbackSupported
),
}, },
abortController abortController
); );
@@ -71,7 +81,7 @@ navigator.credentials.create = async (
return WebauthnUtils.mapCredentialRegistrationResult(response.result); return WebauthnUtils.mapCredentialRegistrationResult(response.result);
} catch (error) { } catch (error) {
if (error && error.fallbackRequested) { if (error && error.fallbackRequested && fallbackSupported) {
return await browserCredentials.create(options); return await browserCredentials.create(options);
} }
@@ -83,12 +93,18 @@ navigator.credentials.get = async (
options?: CredentialRequestOptions, options?: CredentialRequestOptions,
abortController?: AbortController abortController?: AbortController
): Promise<Credential> => { ): Promise<Credential> => {
const fallbackSupported = browserNativeWebauthnSupport;
try { try {
const response = await messenger.request( const response = await messenger.request(
{ {
type: MessageType.CredentialGetRequest, type: MessageType.CredentialGetRequest,
// TODO: Fix sameOriginWithAncestors! // TODO: Fix sameOriginWithAncestors!
data: WebauthnUtils.mapCredentialRequestOptions(options, window.location.origin, true), data: WebauthnUtils.mapCredentialRequestOptions(
options,
window.location.origin,
true,
fallbackSupported
),
}, },
abortController abortController
); );
@@ -99,7 +115,7 @@ navigator.credentials.get = async (
return WebauthnUtils.mapCredentialAssertResult(response.result); return WebauthnUtils.mapCredentialAssertResult(response.result);
} catch (error) { } catch (error) {
if (error && error.fallbackRequested) { if (error && error.fallbackRequested && fallbackSupported) {
return await browserCredentials.get(options); return await browserCredentials.get(options);
} }

View File

@@ -45,7 +45,12 @@
You do not have a matching login for this site. You do not have a matching login for this site.
</ng-container> </ng-container>
</ng-container> </ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="abort(true)"> <button
*ngIf="data.fallbackSupported"
type="button"
class="btn btn-outline-secondary"
(click)="abort(true)"
>
Use browser built-in Use browser built-in
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="abort(false)">Abort</button> <button type="button" class="btn btn-outline-secondary" (click)="abort(false)">Abort</button>

View File

@@ -27,6 +27,7 @@ import {
interface ViewData { interface ViewData {
message: BrowserFido2Message; message: BrowserFido2Message;
showUnsupportedVerification: boolean; showUnsupportedVerification: boolean;
fallbackSupported: boolean;
} }
@Component({ @Component({
@@ -108,11 +109,10 @@ export class Fido2Component implements OnInit, OnDestroy {
return { return {
message, message,
showUnsupportedVerification: showUnsupportedVerification:
(message.type === "ConfirmNewCredentialRequest" || "userVerification" in message &&
message.type === "ConfirmNewNonDiscoverableCredentialRequest" ||
message.type === "PickCredentialRequest") &&
message.userVerification && message.userVerification &&
!(await this.passwordRepromptService.enabled()), !(await this.passwordRepromptService.enabled()),
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
}; };
}), }),
takeUntil(this.destroy$) takeUntil(this.destroy$)

View File

@@ -48,6 +48,7 @@ export type BrowserFido2Message = { sessionId: string } & (
type: "PickCredentialRequest"; type: "PickCredentialRequest";
cipherIds: string[]; cipherIds: string[];
userVerification: boolean; userVerification: boolean;
fallbackSupported: boolean;
} }
| { | {
type: "PickCredentialResponse"; type: "PickCredentialResponse";
@@ -59,6 +60,7 @@ export type BrowserFido2Message = { sessionId: string } & (
credentialName: string; credentialName: string;
userName: string; userName: string;
userVerification: boolean; userVerification: boolean;
fallbackSupported: boolean;
} }
| { | {
type: "ConfirmNewCredentialResponse"; type: "ConfirmNewCredentialResponse";
@@ -69,6 +71,7 @@ export type BrowserFido2Message = { sessionId: string } & (
credentialName: string; credentialName: string;
userName: string; userName: string;
userVerification: boolean; userVerification: boolean;
fallbackSupported: boolean;
} }
| { | {
type: "ConfirmNewNonDiscoverableCredentialResponse"; type: "ConfirmNewNonDiscoverableCredentialResponse";
@@ -78,9 +81,11 @@ export type BrowserFido2Message = { sessionId: string } & (
| { | {
type: "InformExcludedCredentialRequest"; type: "InformExcludedCredentialRequest";
existingCipherIds: string[]; existingCipherIds: string[];
fallbackSupported: boolean;
} }
| { | {
type: "InformCredentialNotFoundRequest"; type: "InformCredentialNotFoundRequest";
fallbackSupported: boolean;
} }
| { | {
type: "AbortRequest"; type: "AbortRequest";
@@ -94,17 +99,29 @@ export type BrowserFido2Message = { sessionId: string } & (
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
constructor(private popupUtilsService: PopupUtilsService) {} constructor(private popupUtilsService: PopupUtilsService) {}
async newSession(abortController?: AbortController): Promise<Fido2UserInterfaceSession> { async newSession(
return await BrowserFido2UserInterfaceSession.create(this.popupUtilsService, abortController); fallbackSupported: boolean,
abortController?: AbortController
): Promise<Fido2UserInterfaceSession> {
return await BrowserFido2UserInterfaceSession.create(
this.popupUtilsService,
fallbackSupported,
abortController
);
} }
} }
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession { export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
static async create( static async create(
popupUtilsService: PopupUtilsService, popupUtilsService: PopupUtilsService,
fallbackSupported: boolean,
abortController?: AbortController abortController?: AbortController
): Promise<BrowserFido2UserInterfaceSession> { ): Promise<BrowserFido2UserInterfaceSession> {
return new BrowserFido2UserInterfaceSession(popupUtilsService, abortController); return new BrowserFido2UserInterfaceSession(
popupUtilsService,
fallbackSupported,
abortController
);
} }
static sendMessage(msg: BrowserFido2Message) { static sendMessage(msg: BrowserFido2Message) {
@@ -121,6 +138,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
private constructor( private constructor(
private readonly popupUtilsService: PopupUtilsService, private readonly popupUtilsService: PopupUtilsService,
private readonly fallbackSupported: boolean,
readonly abortController = new AbortController(), readonly abortController = new AbortController(),
readonly sessionId = Utils.newGuid() readonly sessionId = Utils.newGuid()
) { ) {
@@ -182,6 +200,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
cipherIds, cipherIds,
sessionId: this.sessionId, sessionId: this.sessionId,
userVerification, userVerification,
fallbackSupported: this.fallbackSupported,
}; };
await this.send(data); await this.send(data);
@@ -201,6 +220,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
credentialName, credentialName,
userName, userName,
userVerification, userVerification,
fallbackSupported: this.fallbackSupported,
}; };
await this.send(data); await this.send(data);
@@ -220,6 +240,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
credentialName, credentialName,
userName, userName,
userVerification, userVerification,
fallbackSupported: this.fallbackSupported,
}; };
await this.send(data); await this.send(data);
@@ -233,6 +254,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
type: "InformExcludedCredentialRequest", type: "InformExcludedCredentialRequest",
sessionId: this.sessionId, sessionId: this.sessionId,
existingCipherIds, existingCipherIds,
fallbackSupported: this.fallbackSupported,
}; };
await this.send(data); await this.send(data);
@@ -243,6 +265,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
const data: BrowserFido2Message = { const data: BrowserFido2Message = {
type: "InformCredentialNotFoundRequest", type: "InformCredentialNotFoundRequest",
sessionId: this.sessionId, sessionId: this.sessionId,
fallbackSupported: this.fallbackSupported,
}; };
await this.send(data); await this.send(data);

View File

@@ -84,6 +84,8 @@ export interface Fido2AuthenticatorMakeCredentialsParams {
/** The effective resident key requirement for credential creation, a Boolean value determined by the client. */ /** The effective resident key requirement for credential creation, a Boolean value determined by the client. */
requireResidentKey: boolean; requireResidentKey: boolean;
requireUserVerification: 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. */ /** 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 // 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. */ /** 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 // requireUserPresence: boolean; // Always required
extensions: unknown; extensions: unknown;
/** Forwarded to user interface */
fallbackSupported: boolean;
} }
export interface Fido2AuthenticatorGetAssertionResult { export interface Fido2AuthenticatorGetAssertionResult {

View File

@@ -44,6 +44,7 @@ export interface CreateCredentialParams {
id: string; // b64 encoded id: string; // b64 encoded
displayName: string; displayName: string;
}; };
fallbackSupported: boolean;
timeout?: number; timeout?: number;
} }
@@ -64,6 +65,7 @@ export interface AssertCredentialParams {
userVerification?: UserVerification; userVerification?: UserVerification;
timeout: number; timeout: number;
sameOriginWithAncestors: boolean; sameOriginWithAncestors: boolean;
fallbackSupported: boolean;
} }
export interface AssertCredentialResult { export interface AssertCredentialResult {

View File

@@ -10,7 +10,10 @@ export interface PickCredentialParams {
} }
export abstract class Fido2UserInterfaceService { export abstract class Fido2UserInterfaceService {
newSession: (abortController?: AbortController) => Promise<Fido2UserInterfaceSession>; newSession: (
fallbackSupported: boolean,
abortController?: AbortController
) => Promise<Fido2UserInterfaceSession>;
} }
export abstract class Fido2UserInterfaceSession { export abstract class Fido2UserInterfaceSession {

View File

@@ -43,7 +43,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
params: Fido2AuthenticatorMakeCredentialsParams, params: Fido2AuthenticatorMakeCredentialsParams,
abortController?: AbortController abortController?: AbortController
): Promise<Fido2AuthenticatorMakeCredentialResult> { ): Promise<Fido2AuthenticatorMakeCredentialResult> {
const userInterfaceSession = await this.userInterface.newSession(abortController); const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
abortController
);
try { try {
if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) {
@@ -211,7 +214,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
params: Fido2AuthenticatorGetAssertionParams, params: Fido2AuthenticatorGetAssertionParams,
abortController?: AbortController abortController?: AbortController
): Promise<Fido2AuthenticatorGetAssertionResult> { ): Promise<Fido2AuthenticatorGetAssertionResult> {
const userInterfaceSession = await this.userInterface.newSession(abortController); const userInterfaceSession = await this.userInterface.newSession(
params.fallbackSupported,
abortController
);
try { try {
if ( if (

View File

@@ -127,6 +127,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
id: Fido2Utils.stringToBuffer(params.user.id), id: Fido2Utils.stringToBuffer(params.user.id),
displayName: params.user.displayName, displayName: params.user.displayName,
}, },
fallbackSupported: params.fallbackSupported,
}; };
let makeCredentialResult; let makeCredentialResult;
try { try {
@@ -226,6 +227,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
hash: clientDataHash, hash: clientDataHash,
allowCredentialDescriptorList, allowCredentialDescriptorList,
extensions: {}, extensions: {},
fallbackSupported: params.fallbackSupported,
}; };
let getAssertionResult; let getAssertionResult;