1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00
Files
browser/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts
Alec Rippberger aa1c0ca0ee feat(auth): [PM-8221] implement device verification for unknown devices
Add device verification flow that requires users to enter an OTP when logging in from an unrecognized device. This includes:

- New device verification route and guard
- Email OTP verification component
- Authentication timeout handling

PM-8221
2025-01-23 12:57:48 -06:00

164 lines
4.7 KiB
TypeScript

import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import {
AsyncActionsModule,
ButtonModule,
FormFieldModule,
IconButtonModule,
LinkModule,
ToastService,
} from "@bitwarden/components";
import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service";
import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service";
import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy";
/**
* Component for verifying a new device via a one-time password (OTP).
*/
@Component({
standalone: true,
selector: "app-new-device-verification",
templateUrl: "./new-device-verification.component.html",
imports: [
CommonModule,
ReactiveFormsModule,
AsyncActionsModule,
JslibModule,
ButtonModule,
FormFieldModule,
IconButtonModule,
LinkModule,
],
})
export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
formGroup = this.formBuilder.group({
code: [
"",
{
validators: [Validators.required],
updateOn: "change",
},
],
});
protected disableRequestOTP = false;
private destroy$ = new Subject<void>();
protected authenticationSessionTimeoutRoute = "/authentication-timeout";
constructor(
private router: Router,
private formBuilder: FormBuilder,
private passwordLoginStrategy: PasswordLoginStrategy,
private apiService: ApiService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private logService: LogService,
private toastService: ToastService,
private i18nService: I18nService,
private syncService: SyncService,
private loginEmailService: LoginEmailServiceAbstraction,
) {}
async ngOnInit() {
// Redirect to timeout route if session expires
this.loginStrategyService.authenticationSessionTimeout$
.pipe(takeUntil(this.destroy$))
.subscribe((expired) => {
if (!expired) {
return;
}
try {
void this.router.navigate([this.authenticationSessionTimeoutRoute]);
} catch (err) {
this.logService.error(
`Failed to navigate to ${this.authenticationSessionTimeoutRoute} route`,
err,
);
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Resends the OTP for device verification.
*/
async resendOTP() {
this.disableRequestOTP = true;
try {
const email = await this.loginStrategyService.getEmail();
const masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
if (!email || !masterPasswordHash) {
throw new Error("Missing email or master password hash");
}
await this.apiService.send(
"POST",
"/accounts/resend-new-device-otp",
{
email: email,
masterPasswordHash: masterPasswordHash,
},
false,
false,
);
} catch (e) {
this.logService.error(e);
} finally {
this.disableRequestOTP = false;
}
}
/**
* Submits the OTP for device verification.
*/
submit = async (): Promise<void> => {
const codeControl = this.formGroup.get("code");
if (!codeControl || !codeControl.value) {
return;
}
try {
const authResult = await this.loginStrategyService.logInNewDeviceVerification(
codeControl.value,
);
if (authResult.requiresTwoFactor) {
await this.router.navigate(["/2fa"]);
return;
}
if (authResult.forcePasswordReset) {
await this.router.navigate(["/update-temp-password"]);
return;
}
this.loginEmailService.clearValues();
await this.syncService.fullSync(true);
// If verification succeeds, navigate to vault
await this.router.navigate(["/vault"]);
} catch (e) {
this.logService.error(e);
const errorMessage =
(e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred");
codeControl.setErrors({ serverError: { message: errorMessage } });
}
};
}