1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-5364] Create SSO Login Service and add state ownership (#7485)

* create sso service

* rename sso service to sso-login service

* rename service

* add references to sso login service and update state calls

* fix browser

* fix desktop

* return promises

* remove sso state from account and global objects

* more descriptive org sso identifier method names

* fix sso tests

* fix tests
This commit is contained in:
Jake Fink
2024-02-08 12:44:35 -05:00
committed by GitHub
parent c2ed6383c6
commit 304c492f24
29 changed files with 259 additions and 177 deletions

View File

@@ -0,0 +1,69 @@
export abstract class SsoLoginServiceAbstraction {
/**
* Gets the code verifier used for SSO.
*
* PKCE requires a `code_verifier` to be generated which is then used to derive a `code_challenge`.
* While the `code_challenge` is verified upon return from the SSO provider, the `code_verifier` is
* sent to the server with the `authorization_code` so that the server can derive the same `code_challenge`
* and verify it matches the one sent in the request for the `authorization_code`.
* @see https://datatracker.ietf.org/doc/html/rfc7636
* @returns The code verifier used for SSO.
*/
getCodeVerifier: () => Promise<string>;
/**
* Sets the code verifier used for SSO.
*
* PKCE requires a `code_verifier` to be generated which is then used to derive a `code_challenge`.
* While the `code_challenge` is verified upon return from the SSO provider, the `code_verifier` is
* sent to the server with the `authorization_code` so that the server can derive the same `code_challenge`
* and verify it matches the one sent in the request for the `authorization_code`.
* @see https://datatracker.ietf.org/doc/html/rfc7636
*/
setCodeVerifier: (codeVerifier: string) => Promise<void>;
/**
* Gets the value of the SSO state.
*
* `state` is a parameter used in the Authorization Code Flow of OAuth 2.0 to prevent CSRF attacks. It is an
* opaque value generated on the client and is sent to the authorization server. The authorization server
* returns the `state` in the callback and the client verifies that the value returned matches the value sent.
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
* @returns The SSO state.
*/
getSsoState: () => Promise<string>;
/**
* Sets the value of the SSO state.
*
* `state` is a parameter used in the Authorization Code Flow of OAuth 2.0 to prevent CSRF attacks. It is an
* opaque value generated on the client and is sent to the authorization server. The authorization server
* returns the `state` in the callback and the client verifies that the value returned matches the value sent.
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
*/
setSsoState: (ssoState: string) => Promise<void>;
/**
* Gets the value of the user's organization sso identifier.
*
* This should only be used during the SSO flow to identify the organization that the user is attempting to log in to.
* Do not use this value outside of the SSO login flow.
* @returns The user's organization identifier.
*/
getOrganizationSsoIdentifier: () => Promise<string>;
/**
* Sets the value of the user's organization sso identifier.
*
* This should only be used during the SSO flow to identify the organization that the user is attempting to log in to.
* Do not use this value outside of the SSO login flow.
*/
setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise<void>;
/**
* 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.
*/
getActiveUserOrganizationSsoIdentifier: () => 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>;
}

View File

@@ -0,0 +1,82 @@
import { firstValueFrom } from "rxjs";
import {
ActiveUserState,
GlobalState,
KeyDefinition,
SSO_DISK,
StateProvider,
} from "../../platform/state";
/**
* Uses disk storage so that the code verifier can be persisted across sso redirects.
*/
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", {
deserializer: (state) => state,
});
/**
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
*/
const ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
SSO_DISK,
"organizationSsoIdentifier",
{
deserializer: (organizationIdentifier) => organizationIdentifier,
},
);
export class SsoLoginService {
private codeVerifierState: GlobalState<string>;
private ssoState: GlobalState<string>;
private orgSsoIdentifierState: GlobalState<string>;
private activeUserOrgSsoIdentifierState: ActiveUserState<string>;
constructor(private stateProvider: StateProvider) {
this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER);
this.ssoState = this.stateProvider.getGlobal(SSO_STATE);
this.orgSsoIdentifierState = this.stateProvider.getGlobal(ORGANIZATION_SSO_IDENTIFIER);
this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive(
ORGANIZATION_SSO_IDENTIFIER,
);
}
getCodeVerifier(): Promise<string> {
return firstValueFrom(this.codeVerifierState.state$);
}
async setCodeVerifier(codeVerifier: string): Promise<void> {
await this.codeVerifierState.update((_) => codeVerifier);
}
getSsoState(): Promise<string> {
return firstValueFrom(this.ssoState.state$);
}
async setSsoState(ssoState: string): Promise<void> {
await this.ssoState.update((_) => ssoState);
}
getOrganizationSsoIdentifier(): Promise<string> {
return firstValueFrom(this.orgSsoIdentifierState.state$);
}
async setOrganizationSsoIdentifier(organizationIdentifier: string): Promise<void> {
await this.orgSsoIdentifierState.update((_) => organizationIdentifier);
}
getActiveUserOrganizationSsoIdentifier(): Promise<string> {
return firstValueFrom(this.activeUserOrgSsoIdentifierState.state$);
}
async setActiveUserOrganizationSsoIdentifier(organizationIdentifier: string): Promise<void> {
await this.activeUserOrgSsoIdentifierState.update((_) => organizationIdentifier);
}
}

View File

@@ -460,17 +460,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SettingsService
*/
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
getSsoCodeVerifier: (options?: StorageOptions) => Promise<string>;
setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoOrgIdentifier: (options?: StorageOptions) => Promise<string>;
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoState: (options?: StorageOptions) => Promise<string>;
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
getUserSsoOrganizationIdentifier: (options?: StorageOptions) => Promise<string>;
setUserSsoOrganizationIdentifier: (
value: string | null,
options?: StorageOptions,
) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@@ -377,25 +377,6 @@ export class AccountDecryptionOptions {
}
}
export class LoginState {
ssoOrganizationIdentifier?: string;
constructor(init?: Partial<LoginState>) {
if (init) {
Object.assign(this, init);
}
}
static fromJSON(obj: Jsonify<LoginState>): LoginState {
if (obj == null) {
return null;
}
const loginState = Object.assign(new LoginState(), obj);
return loginState;
}
}
export class Account {
data?: AccountData = new AccountData();
keys?: AccountKeys = new AccountKeys();
@@ -403,7 +384,6 @@ export class Account {
settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens();
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
loginState?: LoginState = new LoginState();
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
constructor(init: Partial<Account>) {
@@ -432,10 +412,6 @@ export class Account {
...new AccountDecryptionOptions(),
...init?.decryptionOptions,
},
loginState: {
...new LoginState(),
...init?.loginState,
},
adminAuthRequest: init?.adminAuthRequest,
});
}
@@ -452,7 +428,6 @@ export class Account {
settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens),
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
loginState: LoginState.fromJSON(json?.loginState),
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
});
}

View File

@@ -6,9 +6,6 @@ export class GlobalState {
installedVersion?: string;
locale?: string;
organizationInvitation?: any;
ssoCodeVerifier?: string;
ssoOrganizationIdentifier?: string;
ssoState?: string;
rememberedEmail?: string;
theme?: ThemeType = ThemeType.System;
window?: WindowState = new WindowState();

View File

@@ -2446,77 +2446,6 @@ export class StateService<
);
}
async getSsoCodeVerifier(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.ssoCodeVerifier;
}
async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.ssoCodeVerifier = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getSsoOrgIdentifier(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.ssoOrganizationIdentifier;
}
async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
globals.ssoOrganizationIdentifier = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
);
}
async getSsoState(options?: StorageOptions): Promise<string> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.ssoState;
}
async setSsoState(value: string, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.ssoState = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getUserSsoOrganizationIdentifier(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.loginState?.ssoOrganizationIdentifier;
}
async setUserSsoOrganizationIdentifier(
value: string | null,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.loginState.ssoOrganizationIdentifier = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))

View File

@@ -23,6 +23,8 @@ export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk"
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
export const GENERATOR_DISK = new StateDefinition("generator", "disk");