PM-7392 - WIP - Enhancing logout callback to consider the logout reason + move show toast logic into logout callback

This commit is contained in:
Jared Snider 2024-05-07 20:37:39 -04:00
parent d0782554f2
commit 706935098b
No known key found for this signature in database
GPG Key ID: A149DDD612516286
15 changed files with 111 additions and 32 deletions

View File

@ -551,6 +551,9 @@
"loggedOut": {
"message": "Logged out"
},
"loggedOutDesc": {
"message": "You have been logged out of your account."
},
"loginExpired": {
"message": "Your login session has expired."
},

View File

@ -1,3 +1,4 @@
import { LogoutReason } from "@bitwarden/auth/common";
import { KeyConnectorService as AbstractKeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
@ -40,7 +41,7 @@ import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.fa
type KeyConnectorServiceFactoryOptions = FactoryOptions & {
keyConnectorServiceOptions: {
logoutCallback: (expired: boolean, userId?: string) => Promise<void>;
logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise<void>;
};
};

View File

@ -9,6 +9,7 @@ import {
AuthRequestService,
LoginEmailServiceAbstraction,
LoginEmailService,
LogoutReason,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -372,8 +373,8 @@ export default class MainBackground {
}
};
const logoutCallback = async (expired: boolean, userId?: UserId) =>
await this.logout(expired, userId);
const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) =>
await this.logout(logoutReason, userId);
this.logService = new ConsoleLogService(false);
this.cryptoFunctionService = new WebCryptoFunctionService(self);
@ -579,7 +580,7 @@ export default class MainBackground {
return Promise.resolve();
},
this.logService,
(expired: boolean) => this.logout(expired),
(logoutReason: LogoutReason) => this.logout(logoutReason),
);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
@ -1209,7 +1210,8 @@ export default class MainBackground {
}
}
async logout(expired: boolean, userId?: UserId) {
// TODO: figure out what this should look like
async logout(logoutReason: LogoutReason, userId?: UserId) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(
map((a) => a?.id),
@ -1279,7 +1281,6 @@ export default class MainBackground {
this.messagingService.send("switchAccountFinish");
} else {
this.messagingService.send("doneLoggingOut", {
expired: expired,
userId: userBeingLoggedOut,
});
}

View File

@ -1,3 +1,4 @@
import { LogoutReason } from "@bitwarden/auth/common";
import { ApiService as AbstractApiService } from "@bitwarden/common/abstractions/api.service";
import { ApiService } from "@bitwarden/common/services/api.service";
@ -26,7 +27,7 @@ import { stateServiceFactory, StateServiceInitOptions } from "./state-service.fa
type ApiServiceFactoryOptions = FactoryOptions & {
apiServiceOptions: {
refreshAccessTokenErrorCallback?: () => Promise<void>;
logoutCallback: (expired: boolean) => Promise<void>;
logoutCallback: (logoutReason: LogoutReason) => Promise<void>;
customUserAgent?: string;
};
};

View File

@ -84,14 +84,6 @@ export class AppComponent implements OnInit, OnDestroy {
tap((msg: any) => {
if (msg.command === "doneLoggingOut") {
this.authService.logOut(async () => {
if (msg.expired) {
this.toastService.showToast({
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
});
}
// 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
this.router.navigate(["home"]);

View File

@ -743,6 +743,9 @@
"loggedOut": {
"message": "Logged out"
},
"loggedOutDesc": {
"message": "You have been logged out of your account."
},
"loginExpired": {
"message": "Your login session has expired."
},

View File

@ -584,6 +584,9 @@
"loggedOut": {
"message": "Logged out"
},
"loggedOutDesc": {
"message": "You have been logged out of your account."
},
"loginExpired": {
"message": "Your login session has expired."
},

View File

@ -1,6 +1,7 @@
import { InjectionToken } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { ClientType } from "@bitwarden/common/enums";
import {
AbstractStorageService,
@ -35,7 +36,7 @@ export const MEMORY_STORAGE = new SafeInjectionToken<AbstractStorageService>("ME
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
export const LOGOUT_CALLBACK = new SafeInjectionToken<
(expired: boolean, userId?: string) => Promise<void>
(logoutReason: LogoutReason, userId?: string) => Promise<void>
>("LOGOUT_CALLBACK");
export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promise<void>>(
"LOCKED_CALLBACK",

View File

@ -1,5 +1,5 @@
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
import { Subject } from "rxjs";
import { firstValueFrom, Subject } from "rxjs";
import {
AuthRequestServiceAbstraction,
@ -13,6 +13,7 @@ import {
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
UserDecryptionOptionsServiceAbstraction,
LogoutReason,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -117,6 +118,7 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
import { ClientType } from "@bitwarden/common/enums";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
@ -234,7 +236,7 @@ import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-
import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { ToastService } from "@bitwarden/components";
import { ToastOptions, ToastService } from "@bitwarden/components";
import {
ImportApiService,
ImportApiServiceAbstraction,
@ -319,9 +321,65 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: LOGOUT_CALLBACK,
useFactory:
(messagingService: MessagingServiceAbstraction) => (expired: boolean, userId?: string) =>
Promise.resolve(messagingService.send("logout", { expired: expired, userId: userId })),
deps: [MessagingServiceAbstraction],
(
messagingService: MessagingServiceAbstraction,
toastService: ToastService,
i18nService: I18nServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
) =>
async (logoutReason: LogoutReason, userId?: string) => {
const isDesktop = platformUtilsService.getClientType() === ClientType.Desktop;
let toastOptions: ToastOptions;
switch (logoutReason) {
case "sessionExpired": {
toastOptions = {
variant: "warning",
title: i18nService.t("loggedOut"),
message: i18nService.t("loginExpired"),
};
break;
}
case "accessTokenUnableToBeDecrypted": {
toastOptions = {
variant: "error",
title: i18nService.t("loggedOut"),
message: i18nService.t("accessTokenUnableToBeDecrypted"),
};
break;
}
case "refreshTokenSecureStorageRetrievalFailure": {
toastOptions = {
variant: "error",
title: i18nService.t("loggedOut"),
message: i18nService.t("refreshTokenSecureStorageRetrievalFailure"),
};
break;
}
default: {
toastOptions = {
variant: "error",
title: i18nService.t("loggedOut"),
message: i18nService.t("loggedOutDesc"),
};
break;
}
}
const activeToast = toastService.showToast(toastOptions);
if (isDesktop) {
// Since desktop has process reload on logout, we need to wait for the toast to be hidden before triggering the logout.
await firstValueFrom(activeToast.onHidden);
}
return Promise.resolve(messagingService.send("logout", { userId: userId }));
},
deps: [
MessagingServiceAbstraction,
ToastService,
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
],
}),
safeProvider({
provide: LOCKED_CALLBACK,

View File

@ -1,4 +1,9 @@
export type LogoutReason =
| "invalidGrantError"
| "vaultTimeout"
| "invalidSecurityStamp"
| "logoutNotification"
| "keyConnectorError"
| "sessionExpired"
| "accessTokenUnableToBeDecrypted"
| "refreshTokenSecureStorageRetrievalFailure";

View File

@ -1,5 +1,7 @@
import { firstValueFrom } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../admin-console/enums";
@ -57,7 +59,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
private logService: LogService,
private organizationService: OrganizationService,
private keyGenerationService: KeyGenerationService,
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>,
private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise<void>,
private stateProvider: StateProvider,
) {
this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR);
@ -192,7 +194,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
if (this.logoutCallback != null) {
// 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
this.logoutCallback(false);
this.logoutCallback("keyConnectorError");
}
throw new Error("Key Connector error");
}

View File

@ -1,5 +1,7 @@
import { firstValueFrom } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
import { OrganizationConnectionType } from "../admin-console/enums";
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
@ -160,7 +162,7 @@ export class ApiService implements ApiServiceAbstraction {
private stateService: StateService,
private refreshAccessTokenErrorCallback: () => Promise<void>,
private logService: LogService,
private logoutCallback: (expired: boolean) => Promise<void>,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private customUserAgent: string = null,
) {
this.device = platformUtilsService.getDevice();
@ -1888,7 +1890,7 @@ export class ApiService implements ApiServiceAbstraction {
responseJson != null &&
responseJson.error === "invalid_grant")
) {
await this.logoutCallback(true);
await this.logoutCallback("invalidGrantError");
return null;
}
}

View File

@ -2,6 +2,8 @@ import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
import { firstValueFrom } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions";
import { ApiService } from "../abstractions/api.service";
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
@ -36,7 +38,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
private appIdService: AppIdService,
private apiService: ApiService,
private environmentService: EnvironmentService,
private logoutCallback: (expired: boolean) => Promise<void>,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private stateService: StateService,
private authService: AuthService,
private authRequestService: AuthRequestServiceAbstraction,
@ -188,7 +190,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
if (isAuthenticated) {
// 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
this.logoutCallback(true);
this.logoutCallback("logoutNotification");
}
break;
case NotificationType.SyncSendCreate:

View File

@ -1,5 +1,7 @@
import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
@ -34,7 +36,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private stateEventRunnerService: StateEventRunnerService,
private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
private loggedOutCallback: (
logoutReason: LogoutReason,
userId?: string,
) => Promise<void> = null,
) {}
async init(checkOnInterval: boolean) {
@ -145,7 +150,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) {
await this.loggedOutCallback(false, userId);
await this.loggedOutCallback("vaultTimeout", userId);
}
}

View File

@ -1,6 +1,6 @@
import { firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "../../../abstractions/api.service";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
@ -71,7 +71,7 @@ export class SyncService implements SyncServiceAbstraction {
private sendApiService: SendApiService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private avatarService: AvatarService,
private logoutCallback: (expired: boolean) => Promise<void>,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private tokenService: TokenService,
) {}
@ -313,7 +313,7 @@ export class SyncService implements SyncServiceAbstraction {
const stamp = await this.tokenService.getSecurityStamp(response.id);
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
await this.logoutCallback("invalidSecurityStamp");
}
throw new Error("Stamp has changed");