From dfe69f77f59cdca24d1c800c43c0e45e6dac449a Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 26 Apr 2023 08:47:35 -0400 Subject: [PATCH] [PM-687] emergency access invite lost during sso (#5199) * [PM-687] refactor observable in base accept component * [PM-687] add emergency access invitation to global state * [PM-687] save invite to state and check on login * [PM-687] move emergency access check above queryParams observable --- .../src/app/common/base.accept.component.ts | 66 +++++++++++-------- .../src/auth/accept-emergency.component.ts | 4 ++ apps/web/src/auth/sso.component.ts | 15 +++++ apps/web/src/auth/two-factor.component.ts | 14 ++++ .../src/auth/components/sso.component.ts | 10 +-- .../auth/components/two-factor.component.ts | 4 +- libs/common/src/abstractions/state.service.ts | 2 + libs/common/src/models/domain/global-state.ts | 1 + libs/common/src/services/state.service.ts | 17 +++++ 9 files changed, 98 insertions(+), 35 deletions(-) diff --git a/apps/web/src/app/common/base.accept.component.ts b/apps/web/src/app/common/base.accept.component.ts index e09e62a271..a9591f0aef 100644 --- a/apps/web/src/app/common/base.accept.component.ts +++ b/apps/web/src/app/common/base.accept.component.ts @@ -1,6 +1,7 @@ import { Directive, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { first } from "rxjs/operators"; +import { Subject } from "rxjs"; +import { first, switchMap, takeUntil } from "rxjs/operators"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -17,6 +18,8 @@ export abstract class BaseAcceptComponent implements OnInit { protected failedShortMessage = "inviteAcceptFailedShort"; protected failedMessage = "inviteAcceptFailed"; + private destroy$ = new Subject(); + constructor( protected router: Router, protected platformUtilService: PlatformUtilsService, @@ -29,36 +32,43 @@ export abstract class BaseAcceptComponent implements OnInit { abstract unauthedHandler(qParams: Params): Promise; ngOnInit() { - // eslint-disable-next-line rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - let error = this.requiredParameters.some((e) => qParams?.[e] == null || qParams[e] === ""); - let errorMessage: string = null; - if (!error) { - this.authed = await this.stateService.getIsAuthenticated(); - this.email = qParams.email; + this.route.queryParams + .pipe( + first(), + switchMap(async (qParams) => { + let error = this.requiredParameters.some( + (e) => qParams?.[e] == null || qParams[e] === "" + ); + let errorMessage: string = null; + if (!error) { + this.authed = await this.stateService.getIsAuthenticated(); + this.email = qParams.email; - if (this.authed) { - try { - await this.authedHandler(qParams); - } catch (e) { - error = true; - errorMessage = e.message; + if (this.authed) { + try { + await this.authedHandler(qParams); + } catch (e) { + error = true; + errorMessage = e.message; + } + } else { + await this.unauthedHandler(qParams); + } } - } else { - await this.unauthedHandler(qParams); - } - } - if (error) { - const message = - errorMessage != null - ? this.i18nService.t(this.failedShortMessage, errorMessage) - : this.i18nService.t(this.failedMessage); - this.platformUtilService.showToast("error", null, message, { timeout: 10000 }); - this.router.navigate(["/"]); - } + if (error) { + const message = + errorMessage != null + ? this.i18nService.t(this.failedShortMessage, errorMessage) + : this.i18nService.t(this.failedMessage); + this.platformUtilService.showToast("error", null, message, { timeout: 10000 }); + this.router.navigate(["/"]); + } - this.loading = false; - }); + this.loading = false; + }), + takeUntil(this.destroy$) + ) + .subscribe(); } } diff --git a/apps/web/src/auth/accept-emergency.component.ts b/apps/web/src/auth/accept-emergency.component.ts index 1904ce4e4e..03c6c9cc8a 100644 --- a/apps/web/src/auth/accept-emergency.component.ts +++ b/apps/web/src/auth/accept-emergency.component.ts @@ -36,6 +36,7 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { request.token = qParams.token; this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request); await this.actionPromise; + await this.stateService.setEmergencyAccessInvitation(null); this.platformUtilService.showToast( "success", this.i18nService.t("inviteAccepted"), @@ -51,5 +52,8 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { // Fix URL encoding of space issue with Angular this.name = this.name.replace(/\+/g, " "); } + + // save the invitation to state so sso logins can find it later + await this.stateService.setEmergencyAccessInvitation(qParams); } } diff --git a/apps/web/src/auth/sso.component.ts b/apps/web/src/auth/sso.component.ts index ddc0d754e4..dec18647b5 100644 --- a/apps/web/src/auth/sso.component.ts +++ b/apps/web/src/auth/sso.component.ts @@ -61,6 +61,21 @@ export class SsoComponent extends BaseSsoComponent { async ngOnInit() { super.ngOnInit(); + // if we have an emergency access invite, redirect to emergency access + const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation(); + if (emergencyAccessInvite != null) { + this.onSuccessfulLoginNavigate = async () => { + this.router.navigate(["/accept-emergency"], { + queryParams: { + id: emergencyAccessInvite.id, + name: emergencyAccessInvite.name, + email: emergencyAccessInvite.email, + token: emergencyAccessInvite.token, + }, + }); + }; + } + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (qParams) => { if (qParams.identifier != null) { diff --git a/apps/web/src/auth/two-factor.component.ts b/apps/web/src/auth/two-factor.component.ts index e972d91e8a..c2c14dc04e 100644 --- a/apps/web/src/auth/two-factor.component.ts +++ b/apps/web/src/auth/two-factor.component.ts @@ -87,6 +87,20 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { if (previousUrl) { this.router.navigateByUrl(previousUrl); } else { + // if we have an emergency access invite, redirect to emergency access + const emergencyAccessInvite = await this.stateService.getEmergencyAccessInvitation(); + if (emergencyAccessInvite != null) { + this.router.navigate(["/accept-emergency"], { + queryParams: { + id: emergencyAccessInvite.id, + name: emergencyAccessInvite.name, + email: emergencyAccessInvite.email, + token: emergencyAccessInvite.token, + }, + }); + return; + } + this.router.navigate([this.successRoute], { queryParams: { identifier: this.identifier, diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 5b132dd0ec..708a484215 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -186,7 +186,7 @@ export class SsoComponent { const response = await this.formPromise; if (response.requiresTwoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { - this.onSuccessfulLoginTwoFactorNavigate(); + await this.onSuccessfulLoginTwoFactorNavigate(); } else { this.router.navigate([this.twoFactorRoute], { queryParams: { @@ -197,7 +197,7 @@ export class SsoComponent { } } else if (response.resetMasterPassword) { if (this.onSuccessfulLoginChangePasswordNavigate != null) { - this.onSuccessfulLoginChangePasswordNavigate(); + await this.onSuccessfulLoginChangePasswordNavigate(); } else { this.router.navigate([this.changePasswordRoute], { queryParams: { @@ -207,7 +207,7 @@ export class SsoComponent { } } else if (response.forcePasswordReset !== ForceResetPasswordReason.None) { if (this.onSuccessfulLoginForceResetNavigate != null) { - this.onSuccessfulLoginForceResetNavigate(); + await this.onSuccessfulLoginForceResetNavigate(); } else { this.router.navigate([this.forcePasswordResetRoute]); } @@ -215,10 +215,10 @@ export class SsoComponent { const disableFavicon = await this.stateService.getDisableFavicon(); await this.stateService.setDisableFavicon(!!disableFavicon); if (this.onSuccessfulLogin != null) { - this.onSuccessfulLogin(); + await this.onSuccessfulLogin(); } if (this.onSuccessfulLoginNavigate != null) { - this.onSuccessfulLoginNavigate(); + await this.onSuccessfulLoginNavigate(); } else { this.router.navigate([this.successRoute]); } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 2a8547a4ec..6573738edc 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -204,7 +204,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } if (this.onSuccessfulLogin != null) { this.loginService.clearValues(); - this.onSuccessfulLogin(); + await this.onSuccessfulLogin(); } if (response.resetMasterPassword) { this.successRoute = "set-password"; @@ -214,7 +214,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } if (this.onSuccessfulLoginNavigate != null) { this.loginService.clearValues(); - this.onSuccessfulLoginNavigate(); + await this.onSuccessfulLoginNavigate(); } else { this.loginService.clearValues(); this.router.navigate([this.successRoute], { diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index d5b1339c3c..57a09d6120 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -298,6 +298,8 @@ export abstract class StateService { setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; + getEmergencyAccessInvitation: (options?: StorageOptions) => Promise; + setEmergencyAccessInvitation: (value: any, options?: StorageOptions) => Promise; /** * @deprecated Do not call this directly, use OrganizationService */ diff --git a/libs/common/src/models/domain/global-state.ts b/libs/common/src/models/domain/global-state.ts index 1a202d5b5f..5ed8557b48 100644 --- a/libs/common/src/models/domain/global-state.ts +++ b/libs/common/src/models/domain/global-state.ts @@ -8,6 +8,7 @@ export class GlobalState { installedVersion?: string; locale?: string; organizationInvitation?: any; + emergencyAccessInvitation?: any; ssoCodeVerifier?: string; ssoOrganizationIdentifier?: string; ssoState?: string; diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 31972b0c7f..8f2e9b7bd3 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -1911,6 +1911,23 @@ export class StateService< ); } + async getEmergencyAccessInvitation(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.emergencyAccessInvitation; + } + + async setEmergencyAccessInvitation(value: any, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.emergencyAccessInvitation = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + /** * @deprecated Do not call this directly, use OrganizationService */