diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a89ac05e4e..6cebe0e231 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -611,6 +611,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 23251e2d58..d2a1ba20bf 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -4,6 +4,7 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthWebAuthnComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component"; @@ -64,6 +65,7 @@ import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts index a07509527e..bb1ef60138 100644 --- a/apps/desktop/src/auth/two-factor-auth.component.ts +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -6,6 +6,7 @@ import { RouterLink } from "@angular/router"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; @@ -39,6 +40,7 @@ import { TypographyModule } from "../../../../libs/components/src/typography"; TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 72d17baa14..c0ce5c17ee 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -627,6 +627,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index 9834529b52..352d935728 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -21,6 +21,7 @@ import { LinkModule, TypographyModule, CheckboxModule, DialogService } from "@bi import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; @@ -54,6 +55,7 @@ import { FormFieldModule } from "../../../../../libs/components/src/form-field"; TwoFactorAuthEmailComponent, TwoFactorAuthAuthenticatorComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], }) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b99f065746..8b8c265653 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5519,6 +5519,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html new file mode 100644 index 0000000000..65a7ef9a50 --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.html @@ -0,0 +1,11 @@ +
+ +
+ +
+

{{ "webAuthnNewTab" | i18n }}

+ +
+
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts new file mode 100644 index 0000000000..d6814fa9c0 --- /dev/null +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts @@ -0,0 +1,131 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; +import { ClientType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + ButtonModule, + LinkModule, + TypographyModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-webauthn", + templateUrl: "two-factor-auth-webauthn.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthWebAuthnComponent { + @Output() token = new EventEmitter(); + + webAuthnReady = false; + webAuthnNewTab = false; + webAuthnSupported = false; + webAuthn: WebAuthnIFrame = null; + + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + @Inject(WINDOW) protected win: Window, + protected environmentService: EnvironmentService, + protected twoFactorService: TwoFactorService, + protected route: ActivatedRoute, + ) { + this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + + if (this.platformUtilsService.getClientType() == ClientType.Browser) { + // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe + this.webAuthnNewTab = true; + } + } + + async ngOnInit(): Promise { + if (this.route.snapshot.paramMap.has("webAuthnResponse")) { + this.token.emit(this.route.snapshot.paramMap.get("webAuthnResponse")); + } + + this.cleanupWebAuthn(); + + if (this.win != null && this.webAuthnSupported) { + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); + this.webAuthn = new WebAuthnIFrame( + this.win, + webVaultUrl, + this.webAuthnNewTab, + this.platformUtilsService, + this.i18nService, + (token: string) => { + this.token.emit(token); + }, + (error: string) => { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("webauthnCancelOrTimeout"), + ); + }, + (info: string) => { + if (info === "ready") { + this.webAuthnReady = true; + } + }, + ); + + if (!this.webAuthnNewTab) { + setTimeout(async () => { + await this.authWebAuthn(); + }, 500); + } + } + } + + ngOnDestroy(): void { + this.cleanupWebAuthn(); + } + + async authWebAuthn() { + const providerData = (await this.twoFactorService.getProviders()).get( + TwoFactorProviderType.WebAuthn, + ); + + if (!this.webAuthnSupported || this.webAuthn == null) { + return; + } + + this.webAuthn.init(providerData); + } + + private cleanupWebAuthn() { + if (this.webAuthn != null) { + this.webAuthn.stop(); + this.webAuthn.cleanup(); + } + } +} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html index 1d29cc5a4f..33a5e291fa 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html @@ -12,6 +12,10 @@ (token)="token = $event" *ngIf="selectedProviderType === providerType.Yubikey" /> + {{ "rememberMe" | i18n }} @@ -31,7 +35,7 @@ buttonType="primary" bitButton bitFormButton - *ngIf="selectedProviderType != null" + *ngIf="selectedProviderType != null && selectedProviderType !== providerType.WebAuthn" > {{ actionButtonText }} diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 9bd2c49006..16a95d6ba2 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -40,6 +40,7 @@ import { CaptchaProtectedComponent } from "../captcha-protected.component"; import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component"; import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "./two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "./two-factor-auth-yubikey.component"; import { TwoFactorOptionsDialogResult, @@ -63,6 +64,7 @@ import { TwoFactorAuthAuthenticatorComponent, TwoFactorAuthEmailComponent, TwoFactorAuthYubikeyComponent, + TwoFactorAuthWebAuthnComponent, ], providers: [I18nPipe], })