mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
refactor(active-user-state-refactor): [PM-12040] Remove ActiveUserStatus For SSO Login Component (#13149)
* refactor(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - First pass of work to update the state. In the middle of testing. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fix for jslib-services.module.ts * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fix main.background.ts * test(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Added simple tests * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Tiny touchups. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Few fixes to resolve comments. * fix(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Changed place where userId is loaded. * test(active-user-state-refactor): [PM-12040] Remove ActiveUserState from SSO Service - Fixed test.
This commit is contained in:
committed by
GitHub
parent
b55468e6a1
commit
0523ce0b40
@@ -809,7 +809,7 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
);
|
||||
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider, this.logService);
|
||||
|
||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy
|
||||
|
||||
async loadNewUserData() {
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
userId: UserId;
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
@@ -96,10 +96,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.userId),
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
@@ -111,7 +111,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier();
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
@@ -167,10 +167,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.userId),
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.userId),
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
@@ -217,7 +217,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
@@ -260,7 +260,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
@@ -269,9 +269,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.userId);
|
||||
await this.keyService.setUserKey(userKey[0], this.userId);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
@@ -280,7 +280,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.userId);
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
@@ -288,6 +288,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
@@ -27,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@@ -55,6 +57,7 @@ export class SsoComponent implements OnInit {
|
||||
protected redirectUri: string;
|
||||
protected state: string;
|
||||
protected codeChallenge: string;
|
||||
protected activeUserId: UserId;
|
||||
|
||||
constructor(
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
@@ -74,7 +77,11 @@ export class SsoComponent implements OnInit {
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
) {
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
@@ -226,7 +233,10 @@ export class SsoComponent implements OnInit {
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
orgSsoIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// Users enrolled in admin acct recovery can be forced to set a new password after
|
||||
// having the admin set a temp password for them (affects TDE & standard users)
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwoFactorMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
|
||||
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
|
||||
@@ -31,6 +32,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -126,6 +128,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
protected changePasswordRoute = "set-password";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected successRoute = "vault";
|
||||
protected activeUserId: UserId;
|
||||
|
||||
constructor(
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
@@ -148,6 +151,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -214,7 +221,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
}
|
||||
}
|
||||
|
||||
async selectOtherTwofactorMethod() {
|
||||
async selectOtherTwoFactorMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||
@@ -262,7 +269,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
this.orgIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -35,6 +35,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
@@ -73,6 +74,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected successRoute = "vault";
|
||||
protected twoFactorTimeoutRoute = "authentication-timeout";
|
||||
|
||||
protected activeUserId: UserId;
|
||||
|
||||
get isDuoProvider(): boolean {
|
||||
return (
|
||||
this.selectedProviderType === TwoFactorProviderType.Duo ||
|
||||
@@ -102,8 +105,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
|
||||
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
|
||||
this.loginStrategyService.authenticationSessionTimeout$
|
||||
.pipe(takeUntilDestroyed())
|
||||
@@ -287,7 +295,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
this.orgIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -799,7 +799,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SsoLoginServiceAbstraction,
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider],
|
||||
deps: [StateProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
|
||||
@@ -202,7 +202,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
});
|
||||
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -89,6 +90,7 @@ export class SsoComponent implements OnInit {
|
||||
protected state: string | undefined;
|
||||
protected codeChallenge: string | undefined;
|
||||
protected clientId: SsoClientType | undefined;
|
||||
protected activeUserId: UserId | undefined;
|
||||
|
||||
formPromise: Promise<AuthResult> | undefined;
|
||||
initiateSsoFormPromise: Promise<SsoPreValidateResponse> | undefined;
|
||||
@@ -130,6 +132,8 @@ export class SsoComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const qParams: QueryParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
// This if statement will pass on the second portion of the SSO flow
|
||||
@@ -384,7 +388,10 @@ export class SsoComponent implements OnInit {
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
orgSsoIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// Users enrolled in admin acct recovery can be forced to set a new password after
|
||||
// having the admin set a temp password for them (affects TDE & standard users)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class SsoLoginServiceAbstraction {
|
||||
/**
|
||||
* Gets the code verifier used for SSO.
|
||||
@@ -74,12 +76,16 @@ export abstract class SsoLoginServiceAbstraction {
|
||||
* Gets the value of the active user's organization sso identifier.
|
||||
*
|
||||
* This should only be used post successful SSO login once the user is initialized.
|
||||
* @param userId The user id for retrieving the org identifier state.
|
||||
*/
|
||||
getActiveUserOrganizationSsoIdentifier: () => Promise<string>;
|
||||
getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise<string>;
|
||||
/**
|
||||
* Sets the value of the active user's organization sso identifier.
|
||||
*
|
||||
* This should only be used post successful SSO login once the user is initialized.
|
||||
*/
|
||||
setActiveUserOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise<void>;
|
||||
setActiveUserOrganizationSsoIdentifier: (
|
||||
organizationIdentifier: string,
|
||||
userId: UserId | undefined,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
94
libs/common/src/auth/services/sso-login.service.spec.ts
Normal file
94
libs/common/src/auth/services/sso-login.service.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
CODE_VERIFIER,
|
||||
GLOBAL_ORGANIZATION_SSO_IDENTIFIER,
|
||||
SSO_EMAIL,
|
||||
SSO_STATE,
|
||||
SsoLoginService,
|
||||
USER_ORGANIZATION_SSO_IDENTIFIER,
|
||||
} from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
|
||||
describe("SSOLoginService ", () => {
|
||||
let sut: SsoLoginService;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
let mockSingleUserStateProvider: FakeStateProvider;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let userId: UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
userId = Utils.newGuid() as UserId;
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
mockSingleUserStateProvider = new FakeStateProvider(accountService);
|
||||
mockLogService = mock<LogService>();
|
||||
|
||||
sut = new SsoLoginService(mockSingleUserStateProvider, mockLogService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
it("gets and sets code verifier", async () => {
|
||||
const codeVerifier = "test-code-verifier";
|
||||
await sut.setCodeVerifier(codeVerifier);
|
||||
mockSingleUserStateProvider.getGlobal(CODE_VERIFIER);
|
||||
|
||||
const result = await sut.getCodeVerifier();
|
||||
expect(result).toBe(codeVerifier);
|
||||
});
|
||||
|
||||
it("gets and sets SSO state", async () => {
|
||||
const ssoState = "test-sso-state";
|
||||
await sut.setSsoState(ssoState);
|
||||
mockSingleUserStateProvider.getGlobal(SSO_STATE);
|
||||
|
||||
const result = await sut.getSsoState();
|
||||
expect(result).toBe(ssoState);
|
||||
});
|
||||
|
||||
it("gets and sets organization SSO identifier", async () => {
|
||||
const orgIdentifier = "test-org-identifier";
|
||||
await sut.setOrganizationSsoIdentifier(orgIdentifier);
|
||||
mockSingleUserStateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER);
|
||||
|
||||
const result = await sut.getOrganizationSsoIdentifier();
|
||||
expect(result).toBe(orgIdentifier);
|
||||
});
|
||||
|
||||
it("gets and sets SSO email", async () => {
|
||||
const email = "test@example.com";
|
||||
await sut.setSsoEmail(email);
|
||||
mockSingleUserStateProvider.getGlobal(SSO_EMAIL);
|
||||
|
||||
const result = await sut.getSsoEmail();
|
||||
expect(result).toBe(email);
|
||||
});
|
||||
|
||||
it("gets and sets active user organization SSO identifier", async () => {
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const orgIdentifier = "test-active-org-identifier";
|
||||
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, userId);
|
||||
mockSingleUserStateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER);
|
||||
|
||||
const result = await sut.getActiveUserOrganizationSsoIdentifier(userId);
|
||||
expect(result).toBe(orgIdentifier);
|
||||
});
|
||||
|
||||
it("logs error when setting active user organization SSO identifier with undefined userId", async () => {
|
||||
const orgIdentifier = "test-active-org-identifier";
|
||||
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined);
|
||||
|
||||
expect(mockLogService.warning).toHaveBeenCalledWith(
|
||||
"Tried to set a user organization sso identifier with an undefined user id.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
ActiveUserState,
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
SingleUserState,
|
||||
SSO_DISK,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
@@ -15,21 +18,21 @@ import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.ab
|
||||
/**
|
||||
* Uses disk storage so that the code verifier can be persisted across sso redirects.
|
||||
*/
|
||||
const CODE_VERIFIER = new KeyDefinition<string>(SSO_DISK, "ssoCodeVerifier", {
|
||||
export const CODE_VERIFIER = new KeyDefinition<string>(SSO_DISK, "ssoCodeVerifier", {
|
||||
deserializer: (codeVerifier) => codeVerifier,
|
||||
});
|
||||
|
||||
/**
|
||||
* Uses disk storage so that the sso state can be persisted across sso redirects.
|
||||
*/
|
||||
const SSO_STATE = new KeyDefinition<string>(SSO_DISK, "ssoState", {
|
||||
export const SSO_STATE = new KeyDefinition<string>(SSO_DISK, "ssoState", {
|
||||
deserializer: (state) => state,
|
||||
});
|
||||
|
||||
/**
|
||||
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
|
||||
*/
|
||||
const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition<string>(
|
||||
export const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition<string>(
|
||||
SSO_DISK,
|
||||
"organizationSsoIdentifier",
|
||||
{
|
||||
@@ -41,7 +44,7 @@ const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition<string>(
|
||||
/**
|
||||
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
|
||||
*/
|
||||
const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
|
||||
export const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
|
||||
SSO_DISK,
|
||||
"organizationSsoIdentifier",
|
||||
{
|
||||
@@ -52,7 +55,7 @@ const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
|
||||
/**
|
||||
* Uses disk storage so that the user's email can be persisted across sso redirects.
|
||||
*/
|
||||
const SSO_EMAIL = new KeyDefinition<string>(SSO_DISK, "ssoEmail", {
|
||||
export const SSO_EMAIL = new KeyDefinition<string>(SSO_DISK, "ssoEmail", {
|
||||
deserializer: (state) => state,
|
||||
});
|
||||
|
||||
@@ -61,16 +64,15 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
|
||||
private ssoState: GlobalState<string>;
|
||||
private orgSsoIdentifierState: GlobalState<string>;
|
||||
private ssoEmailState: GlobalState<string>;
|
||||
private activeUserOrgSsoIdentifierState: ActiveUserState<string>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER);
|
||||
this.ssoState = this.stateProvider.getGlobal(SSO_STATE);
|
||||
this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER);
|
||||
this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL);
|
||||
this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive(
|
||||
USER_ORGANIZATION_SSO_IDENTIFIER,
|
||||
);
|
||||
}
|
||||
|
||||
getCodeVerifier(): Promise<string> {
|
||||
@@ -105,11 +107,24 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
|
||||
await this.ssoEmailState.update((_) => email);
|
||||
}
|
||||
|
||||
getActiveUserOrganizationSsoIdentifier(): Promise<string> {
|
||||
return firstValueFrom(this.activeUserOrgSsoIdentifierState.state$);
|
||||
getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise<string> {
|
||||
return firstValueFrom(this.userOrgSsoIdentifierState(userId).state$);
|
||||
}
|
||||
|
||||
async setActiveUserOrganizationSsoIdentifier(organizationIdentifier: string): Promise<void> {
|
||||
await this.activeUserOrgSsoIdentifierState.update((_) => organizationIdentifier);
|
||||
async setActiveUserOrganizationSsoIdentifier(
|
||||
organizationIdentifier: string,
|
||||
userId: UserId | undefined,
|
||||
): Promise<void> {
|
||||
if (userId === undefined) {
|
||||
this.logService.warning(
|
||||
"Tried to set a user organization sso identifier with an undefined user id.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.userOrgSsoIdentifierState(userId).update((_) => organizationIdentifier);
|
||||
}
|
||||
|
||||
private userOrgSsoIdentifierState(userId: UserId): SingleUserState<string> {
|
||||
return this.stateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StateUpdateOptions } from "./state-update-options";
|
||||
export interface GlobalState<T> {
|
||||
/**
|
||||
* Method for allowing you to manipulate state in an additive way.
|
||||
* @param configureState callback for how you want manipulate this section of state
|
||||
* @param configureState callback for how you want to manipulate this section of state
|
||||
* @param options Defaults given by @see {module:state-update-options#DEFAULT_OPTIONS}
|
||||
* @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true
|
||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface UserState<T> {
|
||||
}
|
||||
|
||||
export const activeMarker: unique symbol = Symbol("active");
|
||||
|
||||
export interface ActiveUserState<T> extends UserState<T> {
|
||||
readonly [activeMarker]: true;
|
||||
|
||||
@@ -32,7 +33,7 @@ export interface ActiveUserState<T> extends UserState<T> {
|
||||
* @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true
|
||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||
|
||||
*
|
||||
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
||||
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
|
||||
*/
|
||||
@@ -41,6 +42,7 @@ export interface ActiveUserState<T> extends UserState<T> {
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
) => Promise<[UserId, T]>;
|
||||
}
|
||||
|
||||
export interface SingleUserState<T> extends UserState<T> {
|
||||
readonly userId: UserId;
|
||||
|
||||
@@ -51,7 +53,7 @@ export interface SingleUserState<T> extends UserState<T> {
|
||||
* @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true
|
||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||
|
||||
*
|
||||
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
||||
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user