mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-5255, PM-3339] Refactor login strategy to use state providers (#7821)
* add key definition and StrategyData classes * use state providers for login strategies * serialize login data for cache * use state providers for auth request notification * fix registrations * add docs to abstraction * fix sso strategy * fix password login strategy tests * fix base login strategy tests * fix user api login strategy tests * PM-3339 add tests for admin auth request in sso strategy * fix auth request login strategy tests * fix webauthn login strategy tests * create login strategy state * use barrel file in common/spec * test login strategy cache deserialization * use global state provider * add test for login strategy service * fix auth request storage * add recursive prototype checking and json deserializers to nested objects * fix CLI * Create wrapper for login strategy cache * use behavior subjects in strategies instead of global state * rename userApi to userApiKey * pr feedback * fix tests * fix deserialization tests * fix tests --------- Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
@@ -19,20 +22,54 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy } from "./login.strategy";
|
||||
import { LoginStrategyData, LoginStrategy } from "./login.strategy";
|
||||
|
||||
export class SsoLoginStrategyData implements LoginStrategyData {
|
||||
captchaBypassToken: string;
|
||||
tokenRequest: SsoTokenRequest;
|
||||
/**
|
||||
* User email address. Only available after authentication.
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The organization ID that the user is logging into. Used for Key Connector
|
||||
* purposes after authentication.
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* A token provided by the server as an authentication factor for sending
|
||||
* email OTPs to the user's configured 2FA email address. This is required
|
||||
* as we don't have a master password hash or other verifiable secret when using SSO.
|
||||
*/
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<SsoLoginStrategyData>): SsoLoginStrategyData {
|
||||
return Object.assign(new SsoLoginStrategyData(), obj, {
|
||||
tokenRequest: SsoTokenRequest.fromJSON(obj.tokenRequest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SsoLoginStrategy extends LoginStrategy {
|
||||
tokenRequest: SsoTokenRequest;
|
||||
orgId: string;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.email}
|
||||
*/
|
||||
email$: Observable<string | null>;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.orgId}
|
||||
*/
|
||||
orgId$: Observable<string>;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
|
||||
*/
|
||||
ssoEmail2FaSessionToken$: Observable<string | null>;
|
||||
|
||||
// A session token server side to serve as an authentication factor for the user
|
||||
// in order to send email OTPs to the user's configured 2FA email address
|
||||
// as we don't have a master password hash or other verifiable secret when using SSO.
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
email?: string; // email not preserved through SSO process so get from server
|
||||
protected cache: BehaviorSubject<SsoLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: SsoLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
@@ -58,11 +95,17 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.email));
|
||||
this.orgId$ = this.cache.pipe(map((state) => state.orgId));
|
||||
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLoginCredentials) {
|
||||
this.orgId = credentials.orgId;
|
||||
this.tokenRequest = new SsoTokenRequest(
|
||||
const data = new SsoLoginStrategyData();
|
||||
data.orgId = credentials.orgId;
|
||||
data.tokenRequest = new SsoTokenRequest(
|
||||
credentials.code,
|
||||
credentials.codeVerifier,
|
||||
credentials.redirectUrl,
|
||||
@@ -70,16 +113,24 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
|
||||
this.cache.next(data);
|
||||
|
||||
const [ssoAuthResult] = await this.startLogIn();
|
||||
|
||||
this.email = ssoAuthResult.email;
|
||||
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||
const email = ssoAuthResult.email;
|
||||
const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||
|
||||
// Auth guard currently handles redirects for this.
|
||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
|
||||
}
|
||||
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
email,
|
||||
ssoEmail2FaSessionToken,
|
||||
});
|
||||
|
||||
return ssoAuthResult;
|
||||
}
|
||||
|
||||
@@ -92,7 +143,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
if (newSsoUser) {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||
tokenResponse,
|
||||
this.cache.value.orgId,
|
||||
);
|
||||
} else {
|
||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
@@ -272,4 +326,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
sso: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user