1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-15 01:11:47 +01:00

PM-8113 - TwoFactorAuth + Webauthn - Refactor logic

This commit is contained in:
Jared Snider 2025-01-23 17:23:52 -05:00
parent a35ab8f2d2
commit 09f4a468c9
No known key found for this signature in database
GPG Key ID: A149DDD612516286
8 changed files with 62 additions and 34 deletions

View File

@ -20,7 +20,7 @@ export class ExtensionTwoFactorAuthComponentService
super();
}
shouldCheckForWebauthnResponseOnInit(): boolean {
shouldCheckForWebAuthnQueryParamResponse(): boolean {
return true;
}

View File

@ -12,6 +12,12 @@ export class ExtensionTwoFactorAuthWebAuthnComponentService
super();
}
/**
* In the browser extension, we open webAuthn in a new web client tab sometimes due to inline
* WebAuthn Iframe's not working in some browsers. We open a 2FA popout upon successful
* completion of WebAuthn submission with query parameters to finish the 2FA process.
* @returns boolean
*/
shouldOpenWebAuthnInNewTab(): boolean {
const isChrome = this.platformUtilsService.isChrome();
if (isChrome) {

View File

@ -4,6 +4,7 @@
export abstract class TwoFactorAuthWebAuthnComponentService {
/**
* Determines if the WebAuthn 2FA should be opened in a new tab or can be completed in the current tab.
* In a browser extension context, we open WebAuthn in a new web client tab.
*/
abstract shouldOpenWebAuthnInNewTab(): boolean;
}

View File

@ -25,6 +25,11 @@ import {
import { TwoFactorAuthWebAuthnComponentService } from "./two-factor-auth-webauthn-component.service";
export interface WebAuthnResult {
token: string;
remember?: boolean;
}
@Component({
standalone: true,
selector: "app-two-factor-auth-webauthn",
@ -44,7 +49,7 @@ import { TwoFactorAuthWebAuthnComponentService } from "./two-factor-auth-webauth
providers: [],
})
export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
@Output() token = new EventEmitter<string>();
@Output() webAuthnResultEmitter = new EventEmitter<WebAuthnResult>();
webAuthnReady = false;
webAuthnNewTab = false;
@ -67,17 +72,26 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
}
async ngOnInit(): Promise<void> {
if (this.route.snapshot.paramMap.has("webAuthnResponse")) {
const webAuthnResponse = this.route.snapshot.paramMap.get("webAuthnResponse");
if (webAuthnResponse != null) {
// TODO: determine if we even need this with the top level processing of the webauthn response.
this.token.emit(webAuthnResponse);
// TODO: should we early return?
// return;
}
if (this.webAuthnNewTab && this.route.snapshot.paramMap.has("webAuthnResponse")) {
this.submitWebAuthnNewTabResponse();
} else {
await this.buildWebAuthnIFrame();
}
}
private submitWebAuthnNewTabResponse() {
const webAuthnNewTabResponse = this.route.snapshot.paramMap.get("webAuthnResponse");
const remember = this.route.snapshot.queryParamMap.get("remember") === "true";
if (webAuthnNewTabResponse != null) {
this.webAuthnResultEmitter.emit({
token: webAuthnNewTabResponse,
remember,
});
}
}
private async buildWebAuthnIFrame() {
if (this.win != null && this.webAuthnSupported) {
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
@ -88,7 +102,7 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
this.platformUtilsService,
this.i18nService,
(token: string) => {
this.token.emit(token);
this.webAuthnResultEmitter.emit({ token });
},
(error: string) => {
this.toastService.showToast({

View File

@ -4,7 +4,7 @@ import {
} from "./two-factor-auth-component.service";
export class DefaultTwoFactorAuthComponentService implements TwoFactorAuthComponentService {
shouldCheckForWebauthnResponseOnInit() {
shouldCheckForWebAuthnQueryParamResponse() {
return false;
}

View File

@ -12,9 +12,9 @@ export enum LegacyKeyMigrationAction {
export abstract class TwoFactorAuthComponentService {
/**
* Determines if the client should check for a webauthn response on init.
* Currently, only the extension should check on init.
* Currently, only the extension should check during component initialization.
*/
abstract shouldCheckForWebauthnResponseOnInit(): boolean;
abstract shouldCheckForWebAuthnQueryParamResponse(): boolean;
/**
* Extends the popup width if required.

View File

@ -19,7 +19,7 @@
*ngIf="selectedProviderType === providerType.Yubikey"
/>
<app-two-factor-auth-webauthn
(token)="token = $event; submitForm()"
(webAuthnResultEmitter)="processWebAuthnResult($event)"
*ngIf="selectedProviderType === providerType.WebAuthn"
/>
<app-two-factor-auth-duo

View File

@ -41,7 +41,10 @@ import {
import { TwoFactorAuthAuthenticatorComponent } from "./child-components/two-factor-auth-authenticator.component";
import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-duo/two-factor-auth-duo.component";
import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component";
import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component";
import {
TwoFactorAuthWebAuthnComponent,
WebAuthnResult,
} from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component";
import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component";
import {
LegacyKeyMigrationAction,
@ -143,12 +146,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
this.listenFor2faSessionTimeout();
if (this.twoFactorAuthComponentService.shouldCheckForWebauthnResponseOnInit()) {
await this.processWebAuthnResponseIfExists();
// TODO: should we return here?
// return;
}
await this.setSelected2faProviderType();
await this.set2faProviderData();
await this.setTitleByTwoFactorProviderType();
@ -170,19 +167,21 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
this.loading = false;
}
private async processWebAuthnResponseIfExists() {
const webAuthn2faResponse = this.activatedRoute.snapshot.queryParamMap.get("webAuthnResponse");
if (webAuthn2faResponse) {
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
await this.set2faProviderData();
this.token = webAuthn2faResponse;
this.remember = this.activatedRoute.snapshot.queryParamMap.get("remember") === "true";
await this.submit();
}
}
private async setSelected2faProviderType() {
const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win);
if (
this.twoFactorAuthComponentService.shouldCheckForWebAuthnQueryParamResponse() &&
webAuthnSupported
) {
const webAuthn2faResponse =
this.activatedRoute.snapshot.queryParamMap.get("webAuthnResponse");
if (webAuthn2faResponse) {
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
return;
}
}
this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported);
}
@ -212,6 +211,14 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
});
}
async processWebAuthnResult(webAuthnResponse: WebAuthnResult) {
this.token = webAuthnResponse.token;
if (webAuthnResponse.remember) {
this.remember = webAuthnResponse.remember;
}
await this.submit();
}
async submit() {
if (this.token == null || this.token === "") {
this.toastService.showToast({