1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-19 01:51:27 +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(); super();
} }
shouldCheckForWebauthnResponseOnInit(): boolean { shouldCheckForWebAuthnQueryParamResponse(): boolean {
return true; return true;
} }

View File

@ -12,6 +12,12 @@ export class ExtensionTwoFactorAuthWebAuthnComponentService
super(); 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 { shouldOpenWebAuthnInNewTab(): boolean {
const isChrome = this.platformUtilsService.isChrome(); const isChrome = this.platformUtilsService.isChrome();
if (isChrome) { if (isChrome) {

View File

@ -4,6 +4,7 @@
export abstract class TwoFactorAuthWebAuthnComponentService { export abstract class TwoFactorAuthWebAuthnComponentService {
/** /**
* Determines if the WebAuthn 2FA should be opened in a new tab or can be completed in the current tab. * 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; abstract shouldOpenWebAuthnInNewTab(): boolean;
} }

View File

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

View File

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

View File

@ -12,9 +12,9 @@ export enum LegacyKeyMigrationAction {
export abstract class TwoFactorAuthComponentService { export abstract class TwoFactorAuthComponentService {
/** /**
* Determines if the client should check for a webauthn response on init. * 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. * Extends the popup width if required.

View File

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

View File

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