mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
fix(Multi-Account-Logout: [Auth/PM-19555] Fix multi account logout on lock screens not redirecting properly (#14630)
* PM-19555 - LogoutService - build abstraction, default, and extension service and register with service modules * PM-19555 - Lock Comp - use logoutService * PM-19555 - LoginDecryptionOptions - Use logout service which removed need for extension-login-decryption-options.service * PM-19555 - AccountSwitcher logic update - (1) Use logout service + redirect guard routing (2) Remove logout method from account switcher service (3) use new NewActiveUser type * PM-19555 - Extension - Acct Switcher comp - clean up TODOs * PM-19555 - Add TODOs for remaining tech debt * PM-19555 - Add tests for new logout services. * PM-19555 - Extension - LoginInitiated - show acct switcher b/c user is AuthN * PM-19555 - Add TODO to replace LogoutCallback with LogoutService * PM-19555 WIP * PM-19555 - Extension App Comp - account switching to account in TDE locked state works now. * PM-19555 - Extension App Comp - add docs * PM-19555 - Extension App Comp - add early return * PM-19555 - Desktop App Comp - add handling for TDE lock case to switch account logic. * PM-19555 - Extension - Account Component - if account unlocked go to vault * PM-19555 - Per PR feedback, clean up unnecessary nullish coalescing operator. * PM-19555 - Extension - AppComponent - fix everHadUserKey merge issue * PM-19555 - PR feedback - refactor switchAccount and locked message handling on browser & desktop to require user id. I audited all callsites for both to ensure this *shouldn't* error.
This commit is contained in:
@@ -4,7 +4,7 @@ import { Router } from "@angular/router";
|
|||||||
import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs";
|
import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { LockService } from "@bitwarden/auth/common";
|
import { LockService, LogoutService } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -69,6 +69,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private lockService: LockService,
|
private lockService: LockService,
|
||||||
|
private logoutService: LogoutService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get accountLimit() {
|
get accountLimit() {
|
||||||
@@ -140,12 +141,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const result = await this.accountSwitcherService.logoutAccount(userId);
|
await this.logoutService.logout(userId);
|
||||||
// unlocked logout responses need to be navigated out of the account switcher.
|
// navigate to root so redirect guard can properly route next active user or null user to correct page
|
||||||
// other responses will be handled by background and app.component
|
await this.router.navigate(["/"]);
|
||||||
if (result?.status === AuthenticationStatus.Unlocked) {
|
|
||||||
this.location.back();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule, Location } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -23,7 +24,7 @@ export class AccountComponent {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountSwitcherService: AccountSwitcherService,
|
private accountSwitcherService: AccountSwitcherService,
|
||||||
private location: Location,
|
private router: Router,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private biometricsService: BiometricsService,
|
private biometricsService: BiometricsService,
|
||||||
@@ -44,8 +45,8 @@ export class AccountComponent {
|
|||||||
|
|
||||||
// Navigate out of account switching for unlocked accounts
|
// Navigate out of account switching for unlocked accounts
|
||||||
// locked or logged out account statuses are handled by background and app.component
|
// locked or logged out account statuses are handled by background and app.component
|
||||||
if (result?.status === AuthenticationStatus.Unlocked) {
|
if (result?.authenticationStatus === AuthenticationStatus.Unlocked) {
|
||||||
this.location.back();
|
await this.router.navigate(["vault"]);
|
||||||
await this.biometricsService.setShouldAutopromptNow(false);
|
await this.biometricsService.setShouldAutopromptNow(false);
|
||||||
} else {
|
} else {
|
||||||
await this.biometricsService.setShouldAutopromptNow(true);
|
await this.biometricsService.setShouldAutopromptNow(true);
|
||||||
|
|||||||
@@ -207,35 +207,4 @@ describe("AccountSwitcherService", () => {
|
|||||||
expect(removeListenerSpy).toBeCalledTimes(1);
|
expect(removeListenerSpy).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("logout", () => {
|
|
||||||
const userId1 = "1" as UserId;
|
|
||||||
const userId2 = "2" as UserId;
|
|
||||||
it("initiates logout", async () => {
|
|
||||||
let listener: (
|
|
||||||
message: { command: string; userId: UserId; status: AuthenticationStatus },
|
|
||||||
sender: unknown,
|
|
||||||
sendResponse: unknown,
|
|
||||||
) => void;
|
|
||||||
jest.spyOn(chrome.runtime.onMessage, "addListener").mockImplementation((addedListener) => {
|
|
||||||
listener = addedListener;
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
|
|
||||||
|
|
||||||
const logoutPromise = accountSwitcherService.logoutAccount(userId1);
|
|
||||||
|
|
||||||
listener(
|
|
||||||
{ command: "switchAccountFinish", userId: userId2, status: AuthenticationStatus.Unlocked },
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await logoutPromise;
|
|
||||||
|
|
||||||
expect(messagingService.send).toHaveBeenCalledWith("logout", { userId: userId1 });
|
|
||||||
expect(result).toEqual({ newUserId: userId2, status: AuthenticationStatus.Unlocked });
|
|
||||||
expect(removeListenerSpy).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
timeout,
|
timeout,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { NewActiveUser } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
@@ -43,7 +44,7 @@ export class AccountSwitcherService {
|
|||||||
SPECIAL_ADD_ACCOUNT_ID = "addAccount";
|
SPECIAL_ADD_ACCOUNT_ID = "addAccount";
|
||||||
availableAccounts$: Observable<AvailableAccount[]>;
|
availableAccounts$: Observable<AvailableAccount[]>;
|
||||||
|
|
||||||
switchAccountFinished$: Observable<{ userId: UserId; status: AuthenticationStatus }>;
|
switchAccountFinished$: Observable<NewActiveUser | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
@@ -118,7 +119,7 @@ export class AccountSwitcherService {
|
|||||||
[message: { command: string; userId: UserId; status: AuthenticationStatus }]
|
[message: { command: string; userId: UserId; status: AuthenticationStatus }]
|
||||||
>(chrome.runtime.onMessage).pipe(
|
>(chrome.runtime.onMessage).pipe(
|
||||||
filter(([message]) => message.command === "switchAccountFinish"),
|
filter(([message]) => message.command === "switchAccountFinish"),
|
||||||
map(([message]) => ({ userId: message.userId, status: message.status })),
|
map(([message]) => ({ userId: message.userId, authenticationStatus: message.status })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,29 +144,9 @@ export class AccountSwitcherService {
|
|||||||
return await switchAccountFinishedPromise;
|
return await switchAccountFinishedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param userId the user id to logout
|
|
||||||
* @returns the userId and status of the that has been switch to due to the logout. null on errors.
|
|
||||||
*/
|
|
||||||
async logoutAccount(
|
|
||||||
userId: UserId,
|
|
||||||
): Promise<{ newUserId: UserId; status: AuthenticationStatus } | null> {
|
|
||||||
// logout creates an account switch to the next up user, which may be null
|
|
||||||
const switchPromise = this.listenForSwitchAccountFinish(null);
|
|
||||||
|
|
||||||
await this.messagingService.send("logout", { userId });
|
|
||||||
|
|
||||||
// wait for account switch to happen, the result will be the new user id and status
|
|
||||||
const result = await switchPromise;
|
|
||||||
return { newUserId: result.userId, status: result.status };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listens for the switchAccountFinish message and returns the userId from the message
|
// Listens for the switchAccountFinish message and returns the userId from the message
|
||||||
// Optionally filters switchAccountFinish to an expected userId
|
// Optionally filters switchAccountFinish to an expected userId
|
||||||
private listenForSwitchAccountFinish(
|
listenForSwitchAccountFinish(expectedUserId: UserId | null): Promise<NewActiveUser | null> {
|
||||||
expectedUserId: UserId | null,
|
|
||||||
): Promise<{ userId: UserId; status: AuthenticationStatus } | null> {
|
|
||||||
return firstValueFrom(
|
return firstValueFrom(
|
||||||
this.switchAccountFinished$.pipe(
|
this.switchAccountFinished$.pipe(
|
||||||
filter(({ userId }) => (expectedUserId ? userId === expectedUserId : true)),
|
filter(({ userId }) => (expectedUserId ? userId === expectedUserId : true)),
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Router } from "@angular/router";
|
|
||||||
import { MockProxy, mock } from "jest-mock-extended";
|
|
||||||
import { BehaviorSubject } from "rxjs";
|
|
||||||
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
|
|
||||||
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
|
||||||
|
|
||||||
import { ExtensionLoginDecryptionOptionsService } from "./extension-login-decryption-options.service";
|
|
||||||
|
|
||||||
// Mock the module providing postLogoutMessageListener$
|
|
||||||
jest.mock("../utils/post-logout-message-listener", () => {
|
|
||||||
return {
|
|
||||||
postLogoutMessageListener$: new BehaviorSubject<string>(""), // Replace with mock subject
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ExtensionLoginDecryptionOptionsService", () => {
|
|
||||||
let service: ExtensionLoginDecryptionOptionsService;
|
|
||||||
|
|
||||||
let messagingService: MockProxy<MessagingService>;
|
|
||||||
let router: MockProxy<Router>;
|
|
||||||
let postLogoutMessageSubject: BehaviorSubject<string>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
messagingService = mock<MessagingService>();
|
|
||||||
router = mock<Router>();
|
|
||||||
|
|
||||||
// Cast postLogoutMessageListener$ to BehaviorSubject for dynamic control
|
|
||||||
postLogoutMessageSubject = postLogoutMessageListener$ as BehaviorSubject<string>;
|
|
||||||
|
|
||||||
service = new ExtensionLoginDecryptionOptionsService(messagingService, router);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should instantiate the service", () => {
|
|
||||||
expect(service).not.toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logOut()", () => {
|
|
||||||
it("should send a logout message", async () => {
|
|
||||||
postLogoutMessageSubject.next("switchAccountFinish");
|
|
||||||
|
|
||||||
await service.logOut();
|
|
||||||
|
|
||||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should navigate to root on 'switchAccountFinish'", async () => {
|
|
||||||
postLogoutMessageSubject.next("switchAccountFinish");
|
|
||||||
|
|
||||||
await service.logOut();
|
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(["/"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not navigate for 'doneLoggingOut'", async () => {
|
|
||||||
postLogoutMessageSubject.next("doneLoggingOut");
|
|
||||||
|
|
||||||
await service.logOut();
|
|
||||||
|
|
||||||
expect(router.navigate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Router } from "@angular/router";
|
|
||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import {
|
|
||||||
DefaultLoginDecryptionOptionsService,
|
|
||||||
LoginDecryptionOptionsService,
|
|
||||||
} from "@bitwarden/auth/angular";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
|
|
||||||
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
|
||||||
|
|
||||||
export class ExtensionLoginDecryptionOptionsService
|
|
||||||
extends DefaultLoginDecryptionOptionsService
|
|
||||||
implements LoginDecryptionOptionsService
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
protected messagingService: MessagingService,
|
|
||||||
private router: Router,
|
|
||||||
) {
|
|
||||||
super(messagingService);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async logOut(): Promise<void> {
|
|
||||||
// start listening for "switchAccountFinish" or "doneLoggingOut"
|
|
||||||
const messagePromise = firstValueFrom(postLogoutMessageListener$);
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
super.logOut();
|
|
||||||
|
|
||||||
// wait for messages
|
|
||||||
const command = await messagePromise;
|
|
||||||
|
|
||||||
// doneLoggingOut already has a message handler that will navigate us
|
|
||||||
if (command === "switchAccountFinish") {
|
|
||||||
await this.router.navigate(["/"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { LogoutReason, LogoutService } from "@bitwarden/auth/common";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||||
|
|
||||||
|
import { ExtensionLogoutService } from "./extension-logout.service";
|
||||||
|
|
||||||
|
describe("ExtensionLogoutService", () => {
|
||||||
|
let logoutService: LogoutService;
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let accountSwitcherService: MockProxy<AccountSwitcherService>;
|
||||||
|
|
||||||
|
let primaryUserId: UserId;
|
||||||
|
let secondaryUserId: UserId;
|
||||||
|
let logoutReason: LogoutReason;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
primaryUserId = "1" as UserId;
|
||||||
|
secondaryUserId = "2" as UserId;
|
||||||
|
logoutReason = "vaultTimeout";
|
||||||
|
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
accountSwitcherService = mock<AccountSwitcherService>();
|
||||||
|
logoutService = new ExtensionLogoutService(messagingService, accountSwitcherService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(logoutService).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logout", () => {
|
||||||
|
describe("No new active user", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
accountSwitcherService.listenForSwitchAccountFinish.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends logout message without a logout reason when not provided", async () => {
|
||||||
|
const result = await logoutService.logout(primaryUserId);
|
||||||
|
|
||||||
|
expect(accountSwitcherService.listenForSwitchAccountFinish).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", { userId: primaryUserId });
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends logout message with a logout reason when provided", async () => {
|
||||||
|
const result = await logoutService.logout(primaryUserId, logoutReason);
|
||||||
|
|
||||||
|
expect(accountSwitcherService.listenForSwitchAccountFinish).toHaveBeenCalledTimes(1);
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", {
|
||||||
|
userId: primaryUserId,
|
||||||
|
logoutReason,
|
||||||
|
});
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("New active user", () => {
|
||||||
|
const newActiveUserAuthenticationStatus = AuthenticationStatus.Unlocked;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
accountSwitcherService.listenForSwitchAccountFinish.mockResolvedValue({
|
||||||
|
userId: secondaryUserId,
|
||||||
|
authenticationStatus: newActiveUserAuthenticationStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends logout message without a logout reason when not provided and returns the new active user", async () => {
|
||||||
|
const result = await logoutService.logout(primaryUserId);
|
||||||
|
|
||||||
|
expect(accountSwitcherService.listenForSwitchAccountFinish).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", { userId: primaryUserId });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
userId: secondaryUserId,
|
||||||
|
authenticationStatus: newActiveUserAuthenticationStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends logout message with a logout reason when provided and returns the new active user", async () => {
|
||||||
|
const result = await logoutService.logout(primaryUserId, logoutReason);
|
||||||
|
|
||||||
|
expect(accountSwitcherService.listenForSwitchAccountFinish).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", {
|
||||||
|
userId: primaryUserId,
|
||||||
|
logoutReason,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
userId: secondaryUserId,
|
||||||
|
authenticationStatus: newActiveUserAuthenticationStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
DefaultLogoutService,
|
||||||
|
LogoutReason,
|
||||||
|
LogoutService,
|
||||||
|
NewActiveUser,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||||
|
|
||||||
|
export class ExtensionLogoutService extends DefaultLogoutService implements LogoutService {
|
||||||
|
constructor(
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
private accountSwitcherService: AccountSwitcherService,
|
||||||
|
) {
|
||||||
|
super(messagingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async logout(
|
||||||
|
userId: UserId,
|
||||||
|
logoutReason?: LogoutReason,
|
||||||
|
): Promise<NewActiveUser | undefined> {
|
||||||
|
// logout can result in an account switch to the next up user
|
||||||
|
const accountSwitchFinishPromise =
|
||||||
|
this.accountSwitcherService.listenForSwitchAccountFinish(null);
|
||||||
|
|
||||||
|
// send the logout message
|
||||||
|
this.messagingService.send("logout", { userId, logoutReason });
|
||||||
|
|
||||||
|
// wait for the account switch to finish
|
||||||
|
const result = await accountSwitchFinishPromise;
|
||||||
|
if (result) {
|
||||||
|
return { userId: result.userId, authenticationStatus: result.authenticationStatus };
|
||||||
|
}
|
||||||
|
// if there is no account switch, return undefined
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1539,6 +1539,7 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: PM-21212 - consolidate the logic of this method into the new ExtensionLogoutService
|
||||||
async logout(logoutReason: LogoutReason, userId?: UserId) {
|
async logout(logoutReason: LogoutReason, userId?: UserId) {
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(
|
this.accountService.activeAccount$.pipe(
|
||||||
|
|||||||
@@ -496,7 +496,8 @@ const routes: Routes = [
|
|||||||
canActivate: [tdeDecryptionRequiredGuard()],
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
data: {
|
data: {
|
||||||
pageIcon: DevicesIcon,
|
pageIcon: DevicesIcon,
|
||||||
},
|
showAcctSwitcher: true,
|
||||||
|
} satisfies ExtensionAnonLayoutWrapperData,
|
||||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ import {
|
|||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, map } from "rxjs";
|
||||||
|
|
||||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
ToastOptions,
|
ToastOptions,
|
||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { BiometricsService, BiometricStateService } from "@bitwarden/key-management";
|
import { BiometricsService, BiometricStateService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
|
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
|
||||||
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
|
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
|
||||||
@@ -82,9 +83,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private biometricsService: BiometricsService,
|
private biometricsService: BiometricsService,
|
||||||
private deviceTrustToastService: DeviceTrustToastService,
|
private deviceTrustToastService: DeviceTrustToastService,
|
||||||
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private keyService: KeyService,
|
||||||
private readonly destoryRef: DestroyRef,
|
private readonly destoryRef: DestroyRef,
|
||||||
private readonly documentLangSetter: DocumentLangSetter,
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
private popupSizeService: PopupSizeService,
|
private popupSizeService: PopupSizeService,
|
||||||
|
private logService: LogService,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
@@ -137,14 +141,38 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
} else if (msg.command === "authBlocked" || msg.command === "goHome") {
|
} else if (msg.command === "authBlocked" || msg.command === "goHome") {
|
||||||
await this.router.navigate(["login"]);
|
await this.router.navigate(["login"]);
|
||||||
} else if (
|
} else if (msg.command === "locked") {
|
||||||
msg.command === "locked" &&
|
if (msg.userId == null) {
|
||||||
(msg.userId == null || msg.userId == this.activeUserId)
|
this.logService.error("'locked' message received without userId.");
|
||||||
) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.userId !== this.activeUserId) {
|
||||||
|
this.logService.error(
|
||||||
|
`'locked' message received with userId ${msg.userId} but active userId is ${this.activeUserId}.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.biometricsService.setShouldAutopromptNow(false);
|
await this.biometricsService.setShouldAutopromptNow(false);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// When user is locked, normally we can just send them the lock screen.
|
||||||
this.router.navigate(["lock"]);
|
// However, for account switching scenarios, we need to consider the TDE lock state.
|
||||||
|
const tdeEnabled = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService
|
||||||
|
.userDecryptionOptionsById$(msg.userId)
|
||||||
|
.pipe(map((decryptionOptions) => decryptionOptions?.trustedDeviceOption != null)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const everHadUserKey = await firstValueFrom(
|
||||||
|
this.keyService.everHadUserKey$(msg.userId),
|
||||||
|
);
|
||||||
|
if (tdeEnabled && !everHadUserKey) {
|
||||||
|
await this.router.navigate(["login-initiated"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["lock"]);
|
||||||
} else if (msg.command === "showDialog") {
|
} else if (msg.command === "showDialog") {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
|
||||||
import { merge, of, Subject } from "rxjs";
|
import { merge, of, Subject } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
@@ -25,7 +24,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperDataService,
|
AnonLayoutWrapperDataService,
|
||||||
LoginComponentService,
|
LoginComponentService,
|
||||||
LoginDecryptionOptionsService,
|
|
||||||
TwoFactorAuthComponentService,
|
TwoFactorAuthComponentService,
|
||||||
TwoFactorAuthEmailComponentService,
|
TwoFactorAuthEmailComponentService,
|
||||||
TwoFactorAuthDuoComponentService,
|
TwoFactorAuthDuoComponentService,
|
||||||
@@ -37,6 +35,7 @@ import {
|
|||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
PinServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
SsoUrlService,
|
SsoUrlService,
|
||||||
|
LogoutService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
@@ -137,11 +136,12 @@ import {
|
|||||||
SshImportPromptService,
|
SshImportPromptService,
|
||||||
} from "@bitwarden/vault";
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
|
||||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||||
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
||||||
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
|
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
|
||||||
import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service";
|
import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service";
|
||||||
import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/extension-two-factor-auth-component.service";
|
import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/extension-two-factor-auth-component.service";
|
||||||
import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service";
|
import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service";
|
||||||
import { ExtensionTwoFactorAuthEmailComponentService } from "../../auth/services/extension-two-factor-auth-email-component.service";
|
import { ExtensionTwoFactorAuthEmailComponentService } from "../../auth/services/extension-two-factor-auth-email-component.service";
|
||||||
@@ -642,6 +642,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LogoutService,
|
||||||
|
useClass: ExtensionLogoutService,
|
||||||
|
deps: [MessagingServiceAbstraction, AccountSwitcherService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CompactModeService,
|
provide: CompactModeService,
|
||||||
useExisting: PopupCompactModeService,
|
useExisting: PopupCompactModeService,
|
||||||
@@ -652,11 +657,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ExtensionSsoComponentService,
|
useClass: ExtensionSsoComponentService,
|
||||||
deps: [SyncService, AuthService, EnvironmentService, I18nServiceAbstraction, LogService],
|
deps: [SyncService, AuthService, EnvironmentService, I18nServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: LoginDecryptionOptionsService,
|
|
||||||
useClass: ExtensionLoginDecryptionOptionsService,
|
|
||||||
deps: [MessagingServiceAbstraction, Router],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SshImportPromptService,
|
provide: SshImportPromptService,
|
||||||
useClass: DefaultSshImportPromptService,
|
useClass: DefaultSshImportPromptService,
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
|||||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||||
import { DESKTOP_SSO_CALLBACK, LogoutReason } from "@bitwarden/auth/common";
|
import {
|
||||||
|
DESKTOP_SSO_CALLBACK,
|
||||||
|
LogoutReason,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -165,6 +169,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private deviceTrustToastService: DeviceTrustToastService,
|
private deviceTrustToastService: DeviceTrustToastService,
|
||||||
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private readonly destroyRef: DestroyRef,
|
private readonly destroyRef: DestroyRef,
|
||||||
private readonly documentLangSetter: DocumentLangSetter,
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
) {
|
) {
|
||||||
@@ -416,15 +421,35 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(["/remove-password"]);
|
this.router.navigate(["/remove-password"]);
|
||||||
break;
|
break;
|
||||||
case "switchAccount": {
|
case "switchAccount": {
|
||||||
if (message.userId != null) {
|
if (message.userId == null) {
|
||||||
await this.accountService.switchAccount(message.userId);
|
this.logService.error("'switchAccount' message received without userId.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.accountService.switchAccount(message.userId);
|
||||||
|
|
||||||
const locked =
|
const locked =
|
||||||
(await this.authService.getAuthStatus(message.userId)) ===
|
(await this.authService.getAuthStatus(message.userId)) ===
|
||||||
AuthenticationStatus.Locked;
|
AuthenticationStatus.Locked;
|
||||||
if (locked) {
|
if (locked) {
|
||||||
this.modalService.closeAll();
|
this.modalService.closeAll();
|
||||||
|
|
||||||
|
// We only have to handle TDE lock on "switchAccount" message scenarios but not normal
|
||||||
|
// lock scenarios since the user will have always decrypted the vault at least once in those cases.
|
||||||
|
const tdeEnabled = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService
|
||||||
|
.userDecryptionOptionsById$(message.userId)
|
||||||
|
.pipe(map((decryptionOptions) => decryptionOptions?.trustedDeviceOption != null)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const everHadUserKey = await firstValueFrom(
|
||||||
|
this.keyService.everHadUserKey$(message.userId),
|
||||||
|
);
|
||||||
|
if (tdeEnabled && !everHadUserKey) {
|
||||||
|
await this.router.navigate(["login-initiated"]);
|
||||||
|
} else {
|
||||||
await this.router.navigate(["lock"]);
|
await this.router.navigate(["lock"]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.messagingService.send("unlocked");
|
this.messagingService.send("unlocked");
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -600,6 +625,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: PM-21212 - consolidate the logic of this method into the new LogoutService
|
||||||
|
// (requires creating a desktop specific implementation of the LogoutService)
|
||||||
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
|
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
|
||||||
// passing null-ish values to us.
|
// passing null-ish values to us.
|
||||||
private async logOut(logoutReason: LogoutReason, userId: UserId) {
|
private async logOut(logoutReason: LogoutReason, userId: UserId) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
DefaultAuthRequestApiService,
|
DefaultAuthRequestApiService,
|
||||||
DefaultLoginSuccessHandlerService,
|
DefaultLoginSuccessHandlerService,
|
||||||
|
DefaultLogoutService,
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
LoginApprovalComponentServiceAbstraction,
|
LoginApprovalComponentServiceAbstraction,
|
||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
LoginSuccessHandlerService,
|
LoginSuccessHandlerService,
|
||||||
LogoutReason,
|
LogoutReason,
|
||||||
|
LogoutService,
|
||||||
PinService,
|
PinService,
|
||||||
PinServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
UserDecryptionOptionsService,
|
UserDecryptionOptionsService,
|
||||||
@@ -405,6 +407,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
provide: STATE_FACTORY,
|
provide: STATE_FACTORY,
|
||||||
useValue: new StateFactory(GlobalState, Account),
|
useValue: new StateFactory(GlobalState, Account),
|
||||||
}),
|
}),
|
||||||
|
// TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: LOGOUT_CALLBACK,
|
provide: LOGOUT_CALLBACK,
|
||||||
useFactory:
|
useFactory:
|
||||||
@@ -1540,6 +1543,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: MasterPasswordApiService,
|
useClass: MasterPasswordApiService,
|
||||||
deps: [ApiServiceAbstraction, LogService],
|
deps: [ApiServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LogoutService,
|
||||||
|
useClass: DefaultLogoutService,
|
||||||
|
deps: [MessagingServiceAbstraction],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DocumentLangSetter,
|
provide: DocumentLangSetter,
|
||||||
useClass: DocumentLangSetter,
|
useClass: DocumentLangSetter,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { catchError, defer, firstValueFrom, from, map, of, switchMap, throwError
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import {
|
import {
|
||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
|
LogoutService,
|
||||||
UserDecryptionOptions,
|
UserDecryptionOptions,
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
@@ -109,6 +110,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
|
private logoutService: LogoutService,
|
||||||
) {
|
) {
|
||||||
this.clientType = this.platformUtilsService.getClientType();
|
this.clientType = this.platformUtilsService.getClientType();
|
||||||
}
|
}
|
||||||
@@ -156,19 +158,17 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleMissingEmail() {
|
private async handleMissingEmail() {
|
||||||
|
// TODO: PM-15174 - the solution for this bug will allow us to show the toast on app re-init after
|
||||||
|
// the user has been logged out and the process reload has occurred.
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: null,
|
title: null,
|
||||||
message: this.i18nService.t("activeUserEmailNotFoundLoggingYouOut"),
|
message: this.i18nService.t("activeUserEmailNotFoundLoggingYouOut"),
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(async () => {
|
await this.logoutService.logout(this.activeAccountId);
|
||||||
// We can't simply redirect to `/login` because the user is authed and the unauthGuard
|
// navigate to root so redirect guard can properly route next active user or null user to correct page
|
||||||
// will prevent navigation. We must logout the user first via messagingService, which
|
await this.router.navigate(["/"]);
|
||||||
// redirects to `/`, which will be handled by the redirectGuard to navigate the user to `/login`.
|
|
||||||
// The timeout just gives the user a chance to see the error toast before process reload runs on logout.
|
|
||||||
await this.loginDecryptionOptionsService.logOut();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private observeAndPersistRememberDeviceValueChanges() {
|
private observeAndPersistRememberDeviceValueChanges() {
|
||||||
@@ -312,7 +312,9 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
|||||||
|
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.messagingService.send("logout", { userId: userId });
|
await this.logoutService.logout(userId);
|
||||||
|
// navigate to root so redirect guard can properly route next active user or null user to correct page
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,4 @@ export abstract class LoginDecryptionOptionsService {
|
|||||||
* Handles client-specific logic that runs after a user was successfully created
|
* Handles client-specific logic that runs after a user was successfully created
|
||||||
*/
|
*/
|
||||||
abstract handleCreateUserSuccess(): Promise<void | null>;
|
abstract handleCreateUserSuccess(): Promise<void | null>;
|
||||||
/**
|
|
||||||
* Logs the user out
|
|
||||||
*/
|
|
||||||
abstract logOut(): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from "./user-decryption-options.service.abstraction";
|
|||||||
export * from "./auth-request.service.abstraction";
|
export * from "./auth-request.service.abstraction";
|
||||||
export * from "./login-approval-component.service.abstraction";
|
export * from "./login-approval-component.service.abstraction";
|
||||||
export * from "./login-success-handler.service";
|
export * from "./login-success-handler.service";
|
||||||
|
export * from "./logout.service";
|
||||||
|
|||||||
19
libs/auth/src/common/abstractions/logout.service.ts
Normal file
19
libs/auth/src/common/abstractions/logout.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { LogoutReason } from "../types";
|
||||||
|
|
||||||
|
export interface NewActiveUser {
|
||||||
|
userId: UserId;
|
||||||
|
authenticationStatus: AuthenticationStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class LogoutService {
|
||||||
|
/**
|
||||||
|
* Logs out the user.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @param logoutReason The optional reason for logging out.
|
||||||
|
* @returns The new active user or undefined if there isn't a new active account.
|
||||||
|
*/
|
||||||
|
abstract logout(userId: UserId, logoutReason?: LogoutReason): Promise<NewActiveUser | undefined>;
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ export * from "./auth-request/auth-request-api.service";
|
|||||||
export * from "./accounts/lock.service";
|
export * from "./accounts/lock.service";
|
||||||
export * from "./login-success-handler/default-login-success-handler.service";
|
export * from "./login-success-handler/default-login-success-handler.service";
|
||||||
export * from "./sso-redirect/sso-url.service";
|
export * from "./sso-redirect/sso-url.service";
|
||||||
|
export * from "./logout/default-logout.service";
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { LogoutService } from "../../abstractions";
|
||||||
|
import { LogoutReason } from "../../types";
|
||||||
|
|
||||||
|
import { DefaultLogoutService } from "./default-logout.service";
|
||||||
|
|
||||||
|
describe("DefaultLogoutService", () => {
|
||||||
|
let logoutService: LogoutService;
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
logoutService = new DefaultLogoutService(messagingService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(logoutService).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logout", () => {
|
||||||
|
it("sends logout message without a logout reason when not provided", async () => {
|
||||||
|
const userId = "1" as UserId;
|
||||||
|
|
||||||
|
await logoutService.logout(userId);
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", { userId });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends logout message with a logout reason when provided", async () => {
|
||||||
|
const userId = "1" as UserId;
|
||||||
|
const logoutReason: LogoutReason = "vaultTimeout";
|
||||||
|
await logoutService.logout(userId, logoutReason);
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout", { userId, logoutReason });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { LogoutService, NewActiveUser } from "../../abstractions/logout.service";
|
||||||
|
import { LogoutReason } from "../../types";
|
||||||
|
|
||||||
|
export class DefaultLogoutService implements LogoutService {
|
||||||
|
constructor(protected messagingService: MessagingService) {}
|
||||||
|
async logout(userId: UserId, logoutReason?: LogoutReason): Promise<NewActiveUser | undefined> {
|
||||||
|
this.messagingService.send("logout", { userId, logoutReason });
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,9 +89,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
|||||||
) {
|
) {
|
||||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
||||||
map((options) => {
|
map((options) => {
|
||||||
// TODO: Eslint upgrade. Please resolve this since the ?? does nothing
|
return options?.trustedDeviceOption != null;
|
||||||
// eslint-disable-next-line no-constant-binary-expression
|
|
||||||
return options?.trustedDeviceOption != null ?? false;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -99,9 +97,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
|||||||
supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean> {
|
supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean> {
|
||||||
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
|
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
|
||||||
map((options) => {
|
map((options) => {
|
||||||
// TODO: Eslint upgrade. Please resolve this since the ?? does nothing
|
return options?.trustedDeviceOption != null;
|
||||||
// eslint-disable-next-line no-constant-binary-expression
|
|
||||||
return options?.trustedDeviceOption != null ?? false;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { LogoutService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -156,6 +156,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||||
|
|
||||||
private biometricService: BiometricsService,
|
private biometricService: BiometricsService,
|
||||||
|
private logoutService: LogoutService,
|
||||||
|
|
||||||
private lockComponentService: LockComponentService,
|
private lockComponentService: LockComponentService,
|
||||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||||
@@ -353,7 +354,9 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (confirmed && this.activeAccount != null) {
|
if (confirmed && this.activeAccount != null) {
|
||||||
this.messagingService.send("logout", { userId: this.activeAccount.id });
|
await this.logoutService.logout(this.activeAccount.id);
|
||||||
|
// navigate to root so redirect guard can properly route next active user or null user to correct page
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user