1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

feat(auth): [PM-3953] generalize copy for login with device flows

Updates UI text and translations for the login with device feature to be more consistent and clear across desktop, browser and web clients. Changes include:

- Updated titles and content for login via auth request components
- Revised translations for device approval modal
- Updated notification titles and alert messages
- Simplified device management URL handling
- Added missing translations across platforms

Resolves PM-3953
This commit is contained in:
Alec Rippberger
2025-01-31 11:54:41 -06:00
committed by GitHub
parent 91509f2f7a
commit 8e70d5b923
16 changed files with 146 additions and 57 deletions

View File

@@ -3123,12 +3123,18 @@
"notificationSentDevice": { "notificationSentDevice": {
"message": "A notification has been sent to your device." "message": "A notification has been sent to your device."
}, },
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "A notification was sent to your device"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"youWillBeNotifiedOnceTheRequestIsApproved": { "youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved" "message": "You will be notified once the request is approved"
}, },
@@ -3138,6 +3144,9 @@
"loginInitiated": { "loginInitiated": {
"message": "Login initiated" "message": "Login initiated"
}, },
"logInRequestSent": {
"message": "Request sent"
},
"exposedMasterPassword": { "exposedMasterPassword": {
"message": "Exposed Master Password" "message": "Exposed Master Password"
}, },

View File

@@ -7,13 +7,20 @@
<div class="content login-page"> <div class="content login-page">
<ng-container *ngIf="state == StateEnum.StandardAuthRequest"> <ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<div> <div>
<p class="lead">{{ "loginInitiated" | i18n }}</p> <p class="lead">{{ "logInRequestSent" | i18n }}</p>
<div> <div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p> <p>
{{ "fingerprintMatchInfo" | i18n }} {{ "notificationSentDevicePart1" | i18n }}
<a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p> </p>
</div> </div>

View File

@@ -437,7 +437,7 @@ const routes: Routes = [
data: { data: {
pageIcon: DevicesIcon, pageIcon: DevicesIcon,
pageTitle: { pageTitle: {
key: "loginInitiated", key: "logInRequestSent",
}, },
pageSubtitle: { pageSubtitle: {
key: "aNotificationWasSentToYourDevice", key: "aNotificationWasSentToYourDevice",

View File

@@ -224,7 +224,7 @@ const routes: Routes = [
data: { data: {
pageIcon: DevicesIcon, pageIcon: DevicesIcon,
pageTitle: { pageTitle: {
key: "loginInitiated", key: "logInRequestSent",
}, },
pageSubtitle: { pageSubtitle: {
key: "aNotificationWasSentToYourDevice", key: "aNotificationWasSentToYourDevice",

View File

@@ -51,15 +51,15 @@ describe("DesktopLoginApprovalComponentService", () => {
it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => { it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => {
const title = "Log in requested"; const title = "Log in requested";
const email = "test@bitwarden.com"; const email = "test@bitwarden.com";
const message = `Confirm login attempt for ${email}`; const message = `Confirm access attempt for ${email}`;
const closeText = "Close"; const closeText = "Close";
const loginApprovalComponent = { email } as LoginApprovalComponent; const loginApprovalComponent = { email } as LoginApprovalComponent;
i18nService.t.mockImplementation((key: string) => { i18nService.t.mockImplementation((key: string) => {
switch (key) { switch (key) {
case "logInRequested": case "accountAccessRequested":
return title; return title;
case "confirmLoginAtemptForMail": case "confirmAccessAttempt":
return message; return message;
case "close": case "close":
return closeText; return closeText;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular"; import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
@@ -15,12 +13,12 @@ export class DesktopLoginApprovalComponentService
super(); super();
} }
async showLoginRequestedAlertIfWindowNotVisible(email: string): Promise<void> { async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
const isVisible = await ipc.platform.isWindowVisible(); const isVisible = await ipc.platform.isWindowVisible();
if (!isVisible) { if (!isVisible) {
await ipc.auth.loginRequest( await ipc.auth.loginRequest(
this.i18nService.t("logInRequested"), this.i18nService.t("accountAccessRequested"),
this.i18nService.t("confirmLoginAtemptForMail", email), this.i18nService.t("confirmAccessAttempt", email),
this.i18nService.t("close"), this.i18nService.t("close"),
); );
} }

View File

@@ -3,15 +3,23 @@
<img class="logo-image" alt="Bitwarden" /> <img class="logo-image" alt="Bitwarden" />
<ng-container *ngIf="state == StateEnum.StandardAuthRequest"> <ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="lead text-center">{{ "loginInitiated" | i18n }}</p> <p class="lead text-center">{{ "logInRequestSent" | i18n }}</p>
<div class="box last"> <div class="box last">
<div class="box-content"> <div class="box-content">
<div class="box-content-row" appBoxRow> <div class="box-content-row" appBoxRow>
<div class="section"> <div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p> <p class="section">
<p> {{ "notificationSentDevicePart1" | i18n }}
{{ "fingerprintMatchInfo" | i18n }} <a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p> </p>
</div> </div>

View File

@@ -2745,14 +2745,23 @@
"loginInitiated": { "loginInitiated": {
"message": "Login initiated" "message": "Login initiated"
}, },
"logInRequestSent": {
"message": "Request sent"
},
"notificationSentDevice": { "notificationSentDevice": {
"message": "A notification has been sent to your device." "message": "A notification has been sent to your device."
}, },
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "A notification was sent to your device"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "notificationSentDevicePart1": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" "message": "Unlock Bitwarden on your device or on the "
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
}, },
"needAnotherOptionV1": { "needAnotherOptionV1": {
"message": "Need another option?" "message": "Need another option?"
@@ -2782,11 +2791,11 @@
"message": "Toggle character count", "message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password." "description": "'Character count' describes a feature that displays a number next to each character of the password."
}, },
"areYouTryingtoLogin": { "areYouTryingToAccessYourAccount": {
"message": "Are you trying to log in?" "message": "Are you trying to access your account?"
}, },
"logInAttemptBy": { "accessAttemptBy": {
"message": "Login attempt by $EMAIL$", "message": "Access attempt by $EMAIL$",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -2803,11 +2812,11 @@
"time": { "time": {
"message": "Time" "message": "Time"
}, },
"confirmLogIn": { "confirmAccess": {
"message": "Confirm login" "message": "Confirm access"
}, },
"denyLogIn": { "denyAccess": {
"message": "Deny login" "message": "Deny access"
}, },
"logInConfirmedForEmailOnDevice": { "logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$", "message": "Login confirmed for $EMAIL$ on $DEVICE$",
@@ -2843,8 +2852,8 @@
"thisRequestIsNoLongerValid": { "thisRequestIsNoLongerValid": {
"message": "This request is no longer valid." "message": "This request is no longer valid."
}, },
"confirmLoginAtemptForMail": { "confirmAccessAttempt": {
"message": "Confirm login attempt for $EMAIL$", "message": "Confirm access attempt for $EMAIL$",
"placeholders": { "placeholders": {
"email": { "email": {
"content": "$1", "content": "$1",
@@ -2855,6 +2864,9 @@
"logInRequested": { "logInRequested": {
"message": "Log in requested" "message": "Log in requested"
}, },
"accountAccessRequested": {
"message": "Account access requested"
},
"creatingAccountOn": { "creatingAccountOn": {
"message": "Creating account on" "message": "Creating account on"
}, },

View File

@@ -1,5 +1,3 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<div <div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8" class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
> >
@@ -14,15 +12,11 @@
<div <div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6" class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
> >
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "loginInitiated" | i18n }}</h2> <h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInRequestSent" | i18n }}</h2>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<p class="tw-mb-6"> <p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }} {{ "notificationSentDeviceComplete" | i18n }}
</p> </p>
</div>
<div class="tw-mb-6"> <div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4> <h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
@@ -39,7 +33,7 @@
<hr /> <hr />
<div class="tw-text-light tw-mt-3"> <div class="tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }} {{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a> <a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div> </div>
@@ -52,7 +46,7 @@
> >
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2> <h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
<div class="tw-text-light"> <div>
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p> <p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p> <p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div> </div>
@@ -66,7 +60,7 @@
<hr /> <hr />
<div class="tw-text-light tw-mt-3"> <div class="tw-mt-3">
{{ "troubleLoggingIn" | i18n }} {{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a> <a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div> </div>

View File

@@ -187,7 +187,7 @@ const routes: Routes = [
data: { data: {
pageIcon: DevicesIcon, pageIcon: DevicesIcon,
pageTitle: { pageTitle: {
key: "loginInitiated", key: "logInRequestSent",
}, },
pageSubtitle: { pageSubtitle: {
key: "aNotificationWasSentToYourDevice", key: "aNotificationWasSentToYourDevice",

View File

@@ -1203,6 +1203,9 @@
"logInInitiated": { "logInInitiated": {
"message": "Log in initiated" "message": "Log in initiated"
}, },
"logInRequestSent": {
"message": "Request sent"
},
"submit": { "submit": {
"message": "Submit" "message": "Submit"
}, },
@@ -1392,12 +1395,39 @@
"notificationSentDevice": { "notificationSentDevice": {
"message": "A notification has been sent to your device." "message": "A notification has been sent to your device."
}, },
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": { "aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device" "message": "A notification was sent to your device"
}, },
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": { "versionNumber": {
"message": "Version $VERSION_NUMBER$", "message": "Version $VERSION_NUMBER$",
"placeholders": { "placeholders": {

View File

@@ -64,11 +64,12 @@ export class LoginViaAuthRequestComponentV1
protected StateEnum = State; protected StateEnum = State;
protected state = State.StandardAuthRequest; protected state = State.StandardAuthRequest;
protected webVaultUrl: string;
protected twoFactorRoute = "2fa"; protected twoFactorRoute = "2fa";
protected successRoute = "vault"; protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password"; protected forcePasswordResetRoute = "update-temp-password";
private resendTimeout = 12000; private resendTimeout = 12000;
protected deviceManagementUrl: string;
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
@@ -95,6 +96,12 @@ export class LoginViaAuthRequestComponentV1
) { ) {
super(environmentService, i18nService, platformUtilsService, toastService); super(environmentService, i18nService, platformUtilsService, toastService);
// Get the web vault URL from the environment service
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
// Gets signalR push notification // Gets signalR push notification
// Only fires on approval to prevent enumeration // Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$ this.authRequestService.authRequestPushNotification$

View File

@@ -1,5 +1,5 @@
<bit-dialog> <bit-dialog>
<span bitDialogTitle>{{ "areYouTryingtoLogin" | i18n }}</span> <span bitDialogTitle>{{ "areYouTryingToAccessYourAccount" | i18n }}</span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading"> <div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
@@ -8,7 +8,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="!loading"> <ng-container *ngIf="!loading">
<h4 class="tw-mb-3">{{ "logInAttemptBy" | i18n: email }}</h4> <h4 class="tw-mb-3">{{ "accessAttemptBy" | i18n: email }}</h4>
<div> <div>
<b>{{ "fingerprintPhraseHeader" | i18n }}</b> <b>{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="tw-text-code">{{ fingerprintPhrase }}</p> <p class="tw-text-code">{{ fingerprintPhrase }}</p>
@@ -35,7 +35,7 @@
[bitAction]="approveLogin" [bitAction]="approveLogin"
[disabled]="loading" [disabled]="loading"
> >
{{ "confirmLogIn" | i18n }} {{ "confirmAccess" | i18n }}
</button> </button>
<button <button
bitButton bitButton
@@ -44,7 +44,7 @@
[bitAction]="denyLogin" [bitAction]="denyLogin"
[disabled]="loading" [disabled]="loading"
> >
{{ "denyLogIn" | i18n }} {{ "denyAccess" | i18n }}
</button> </button>
</ng-container> </ng-container>
</bit-dialog> </bit-dialog>

View File

@@ -85,7 +85,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
} }
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
this.email = await await firstValueFrom( this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)), this.accountService.activeAccount$.pipe(map((a) => a?.email)),
); );
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(

View File

@@ -1,6 +1,20 @@
<div class="tw-text-center"> <div class="tw-text-center">
<ng-container *ngIf="flow === Flow.StandardAuthRequest"> <ng-container *ngIf="flow === Flow.StandardAuthRequest">
<p>{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}</p> <p *ngIf="clientType !== ClientType.Web">
{{ "notificationSentDevicePart1" | i18n }}
<a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p>
<p *ngIf="clientType === ClientType.Web">
{{ "notificationSentDeviceComplete" | i18n }}
</p>
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div> <div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
<code class="tw-text-code">{{ fingerprintPhrase }}</code> <code class="tw-text-code">{{ fingerprintPhrase }}</code>

View File

@@ -29,6 +29,7 @@ import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -71,6 +72,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
protected showResendNotification = false; protected showResendNotification = false;
protected Flow = Flow; protected Flow = Flow;
protected flow = Flow.StandardAuthRequest; protected flow = Flow.StandardAuthRequest;
protected webVaultUrl: string;
protected deviceManagementUrl: string;
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
@@ -81,6 +84,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private authService: AuthService, private authService: AuthService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private deviceTrustService: DeviceTrustServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction,
private environmentService: EnvironmentService,
private i18nService: I18nService, private i18nService: I18nService,
private logService: LogService, private logService: LogService,
private loginEmailService: LoginEmailServiceAbstraction, private loginEmailService: LoginEmailServiceAbstraction,
@@ -109,6 +113,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
this.logService.error("Failed to use approved auth request: " + e.message); this.logService.error("Failed to use approved auth request: " + e.message);
}); });
}); });
// Get the web vault URL from the environment service
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {