Add shared webauthn component (#9771)

This commit is contained in:
Bernd Schoolmann 2024-07-16 16:46:37 +02:00 committed by GitHub
parent 96538a68fd
commit 69a37a884f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 164 additions and 1 deletions

View File

@ -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"
},

View File

@ -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],
})

View File

@ -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],
})

View File

@ -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"
},

View File

@ -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],
})

View File

@ -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"
},

View File

@ -0,0 +1,11 @@
<div id="web-authn-frame" class="tw-mb-3" *ngIf="!webAuthnNewTab">
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
<ng-container *ngIf="webAuthnNewTab">
<div class="content text-center" *ngIf="webAuthnNewTab">
<p class="text-center">{{ "webAuthnNewTab" | i18n }}</p>
<button type="button" class="btn primary block" (click)="authWebAuthn()" appStopClick>
{{ "webAuthnNewTabOpen" | i18n }}
</button>
</div>
</ng-container>

View File

@ -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<string>();
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<void> {
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();
}
}
}

View File

@ -12,6 +12,10 @@
(token)="token = $event"
*ngIf="selectedProviderType === providerType.Yubikey"
/>
<app-two-factor-auth-webauthn
(token)="token = $event; submitForm()"
*ngIf="selectedProviderType === providerType.WebAuthn"
/>
<bit-form-control *ngIf="selectedProviderType != null">
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="remember" />
@ -31,7 +35,7 @@
buttonType="primary"
bitButton
bitFormButton
*ngIf="selectedProviderType != null"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.WebAuthn"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
</button>

View File

@ -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],
})