mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-23 02:31:26 +01:00
feat(auth): [PM-15534] log user in when submitting recovery code
- Add recovery code enum and feature flag - Update recovery code text and warning messages - Log user in and redirect to two-factor settings page on valid recovery code - Run full sync and handle login errors silently - Move updated messaging behind feature flag PM-15534
This commit is contained in:
parent
4c09c22806
commit
fa8ee6fa02
@ -18,6 +18,8 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@ -41,6 +43,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
|
||||
private organizationService: OrganizationService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
configService: ConfigService,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
super(
|
||||
dialogService,
|
||||
@ -49,6 +53,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
|
||||
policyService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
configService,
|
||||
i18nService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<p bitTypography="body1">
|
||||
{{ "recoverAccountTwoStepDesc" | i18n }}
|
||||
{{ recoveryCodeMessage }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/lost-two-step-device/"
|
||||
|
@ -1,13 +1,20 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
LoginSuccessHandlerService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@ -16,13 +23,23 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
selector: "app-recover-two-factor",
|
||||
templateUrl: "recover-two-factor.component.html",
|
||||
})
|
||||
export class RecoverTwoFactorComponent {
|
||||
export class RecoverTwoFactorComponent implements OnInit {
|
||||
protected formGroup = new FormGroup({
|
||||
email: new FormControl(null, [Validators.required]),
|
||||
masterPassword: new FormControl(null, [Validators.required]),
|
||||
recoveryCode: new FormControl(null, [Validators.required]),
|
||||
email: new FormControl("", [Validators.required]),
|
||||
masterPassword: new FormControl("", [Validators.required]),
|
||||
recoveryCode: new FormControl("", [Validators.required]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Message to display to the user about the recovery code
|
||||
*/
|
||||
recoveryCodeMessage = "";
|
||||
|
||||
/**
|
||||
* Whether the recovery code login feature flag is enabled
|
||||
*/
|
||||
private recoveryCodeLoginFeatureFlagEnabled = false;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
@ -31,20 +48,35 @@ export class RecoverTwoFactorComponent {
|
||||
private keyService: KeyService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RecoveryCodeLogin,
|
||||
);
|
||||
this.recoveryCodeMessage = this.recoveryCodeLoginFeatureFlagEnabled
|
||||
? this.i18nService.t("logInBelowUsingYourSingleUseRecoveryCode")
|
||||
: this.i18nService.t("recoverAccountTwoStepDesc");
|
||||
}
|
||||
|
||||
get email(): string {
|
||||
return this.formGroup.value.email;
|
||||
return this.formGroup.get("email")?.value ?? "";
|
||||
}
|
||||
|
||||
get masterPassword(): string {
|
||||
return this.formGroup.value.masterPassword;
|
||||
return this.formGroup.get("masterPassword")?.value ?? "";
|
||||
}
|
||||
|
||||
get recoveryCode(): string {
|
||||
return this.formGroup.value.recoveryCode;
|
||||
return this.formGroup.get("recoveryCode")?.value ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of the recovery code form.
|
||||
*/
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
@ -56,12 +88,90 @@ export class RecoverTwoFactorComponent {
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email);
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key);
|
||||
await this.apiService.postTwoFactorRecover(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("twoStepRecoverDisabled"),
|
||||
});
|
||||
await this.router.navigate(["/"]);
|
||||
|
||||
try {
|
||||
await this.apiService.postTwoFactorRecover(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("twoStepRecoverDisabled"),
|
||||
});
|
||||
|
||||
if (!this.recoveryCodeLoginFeatureFlagEnabled) {
|
||||
await this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle login after recovery if the feature flag is enabled
|
||||
await this.handleRecoveryLogin(request);
|
||||
} catch (e) {
|
||||
const errorMessage = this.extractErrorMessage(e);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the login process after a successful account recovery.
|
||||
*/
|
||||
private async handleRecoveryLogin(request: TwoFactorRecoveryRequest) {
|
||||
// Build two-factor request to pass into PasswordLoginCredentials request using the 2FA recovery code and RecoveryCode type
|
||||
const twoFactorRequest: TokenTwoFactorRequest = {
|
||||
provider: TwoFactorProviderType.RecoveryCode,
|
||||
token: request.recoveryCode,
|
||||
remember: false,
|
||||
};
|
||||
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
request.email,
|
||||
this.masterPassword,
|
||||
"",
|
||||
twoFactorRequest,
|
||||
);
|
||||
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("youHaveBeenLoggedIn"),
|
||||
});
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
await this.router.navigate(["/settings/security/two-factor"]);
|
||||
} catch (error) {
|
||||
// If login errors, redirect to login page per product. Don't show error
|
||||
this.logService.error("Error logging in automatically: ", (error as Error).message);
|
||||
await this.router.navigate(["/login"], { queryParams: { email: request.email } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an error message from the error object.
|
||||
*/
|
||||
private extractErrorMessage(error: unknown): string {
|
||||
let errorMessage: string = this.i18nService.t("unexpectedError");
|
||||
if (error && typeof error === "object" && "validationErrors" in error) {
|
||||
const validationErrors = error.validationErrors;
|
||||
if (validationErrors && typeof validationErrors === "object") {
|
||||
errorMessage = Object.keys(validationErrors)
|
||||
.map((key) => {
|
||||
const messages = (validationErrors as Record<string, string | string[]>)[key];
|
||||
return Array.isArray(messages) ? messages.join(" ") : messages;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
} else if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"message" in error &&
|
||||
typeof error.message === "string"
|
||||
) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
</p>
|
||||
</ng-container>
|
||||
<bit-callout type="warning" *ngIf="!organizationId">
|
||||
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
|
||||
<p>{{ recoveryCodeWarningMessage }}</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="recoveryCode()">
|
||||
{{ "viewRecoveryCode" | i18n }}
|
||||
</button>
|
||||
|
@ -29,6 +29,9 @@ import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.s
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@ -52,6 +55,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
providers: any[] = [];
|
||||
canAccessPremium$: Observable<boolean>;
|
||||
recoveryCodeWarningMessage: string;
|
||||
showPolicyWarning = false;
|
||||
loading = true;
|
||||
modal: ModalRef;
|
||||
@ -70,6 +74,8 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
protected policyService: PolicyService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
protected configService: ConfigService,
|
||||
protected i18nService: I18nService,
|
||||
) {
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@ -79,6 +85,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.RecoveryCodeLogin,
|
||||
);
|
||||
this.recoveryCodeWarningMessage = recoveryCodeLoginFeatureFlagEnabled
|
||||
? this.i18nService.t("yourSingleUseRecoveryCode")
|
||||
: this.i18nService.t("twoStepLoginRecoveryWarning");
|
||||
|
||||
for (const key in TwoFactorProviders) {
|
||||
// eslint-disable-next-line
|
||||
if (!TwoFactorProviders.hasOwnProperty(key)) {
|
||||
|
@ -2183,6 +2183,9 @@
|
||||
"twoStepLoginRecoveryWarning": {
|
||||
"message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place."
|
||||
},
|
||||
"yourSingleUseRecoveryCode": {
|
||||
"message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place."
|
||||
},
|
||||
"viewRecoveryCode": {
|
||||
"message": "View recovery code"
|
||||
},
|
||||
@ -4193,6 +4196,9 @@
|
||||
"recoverAccountTwoStepDesc": {
|
||||
"message": "If you cannot access your account through your normal two-step login methods, you can use your two-step login recovery code to turn off all two-step providers on your account."
|
||||
},
|
||||
"logInBelowUsingYourSingleUseRecoveryCode": {
|
||||
"message": "Log in below using your single-use recovery code. This will turn off all two-step providers on your account."
|
||||
},
|
||||
"recoverAccountTwoStep": {
|
||||
"message": "Recover account two-step login"
|
||||
},
|
||||
|
@ -7,4 +7,5 @@ export enum TwoFactorProviderType {
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6,
|
||||
WebAuthn = 7,
|
||||
RecoveryCode = 8,
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ export enum FeatureFlag {
|
||||
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
|
||||
NewDeviceVerification = "new-device-verification",
|
||||
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
|
||||
RecoveryCodeLogin = "pm-17128-recovery-code-login",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerification]: FALSE,
|
||||
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
|
||||
[FeatureFlag.RecoveryCodeLogin]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
Loading…
Reference in New Issue
Block a user