[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 259 additions and 177 deletions

View File

@ -0,0 +1,28 @@
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type SsoLoginServiceFactoryOptions = FactoryOptions;
export type SsoLoginServiceInitOptions = SsoLoginServiceFactoryOptions & StateProviderInitOptions;
export function ssoLoginServiceFactory(
cache: { ssoLoginService?: SsoLoginServiceAbstraction } & CachedServices,
opts: SsoLoginServiceInitOptions,
): Promise<SsoLoginServiceAbstraction> {
return factory(
cache,
"ssoLoginService",
opts,
async () => new SsoLoginService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -7,6 +7,7 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -45,6 +46,7 @@ export class LoginComponent extends BaseLoginComponent {
formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService,
ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
super(
@ -64,6 +66,7 @@ export class LoginComponent extends BaseLoginComponent {
formValidationErrorService,
route,
loginService,
ssoLoginService,
webAuthnLoginService,
);
super.onSuccessfulLogin = async () => {
@ -106,8 +109,8 @@ export class LoginComponent extends BaseLoginComponent {
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.stateService.setSsoCodeVerifier(codeVerifier);
await this.stateService.setSsoState(state);
await this.ssoLoginService.setCodeVerifier(codeVerifier);
await this.ssoLoginService.setSsoState(state);
let url = this.environmentService.getWebVaultUrl();
if (url == null) {

View File

@ -7,6 +7,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -36,6 +37,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
route: ActivatedRoute,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
super(
@ -53,6 +55,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
stateService,
organizationApiService,
organizationUserService,
ssoLoginService,
dialogService,
);
}

View File

@ -6,6 +6,7 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -25,6 +26,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
})
export class SsoComponent extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
@ -42,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent {
@Inject(WINDOW) private win: Window,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,

View File

@ -8,6 +8,7 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
@ -55,6 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
appIdService: AppIdService,
loginService: LoginService,
configService: ConfigServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
private dialogService: DialogService,
@Inject(WINDOW) protected win: Window,
private browserMessagingApi: ZonedMessageListenerService,
@ -73,6 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService,
appIdService,
loginService,
ssoLoginService,
configService,
);
super.onSuccessfulLogin = async () => {

View File

@ -10,6 +10,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -73,6 +74,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService,
ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
super(
@ -92,6 +94,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
formValidationErrorService,
route,
loginService,
ssoLoginService,
webAuthnLoginService,
);
super.onSuccessfulLogin = () => {

View File

@ -7,6 +7,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -43,6 +44,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
super(
@ -60,6 +62,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
stateService,
organizationApiService,
organizationUserService,
ssoLoginService,
dialogService,
);
}

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -20,6 +21,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
})
export class SsoComponent extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
@ -35,6 +37,7 @@ export class SsoComponent extends BaseSsoComponent {
configService: ConfigServiceAbstraction,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,

View File

@ -7,6 +7,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
@ -46,6 +47,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window,
) {
@ -63,6 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService,
appIdService,
loginService,
ssoLoginService,
configService,
);
super.onSuccessfulLogin = async () => {

View File

@ -15,6 +15,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
@ -64,6 +65,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService,
loginService: LoginService,
ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
super(
@ -83,6 +85,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
formValidationErrorService,
route,
loginService,
ssoLoginService,
webAuthnLoginService,
);
this.onSuccessfulLogin = async () => {

View File

@ -1,59 +1,9 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor(
apiService: ApiService,
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
platformUtilsService: PlatformUtilsService,
policyApiService: PolicyApiServiceAbstraction,
policyService: PolicyService,
router: Router,
syncService: SyncService,
route: ActivatedRoute,
stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
dialogService: DialogService,
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyApiService,
policyService,
router,
apiService,
syncService,
route,
stateService,
organizationApiService,
organizationUserService,
dialogService,
);
}
}
export class SetPasswordComponent extends BaseSetPasswordComponent {}

View File

@ -7,6 +7,7 @@ import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
@ -26,6 +27,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SsoComponent extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
@ -42,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent {
configService: ConfigServiceAbstraction,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,
@ -94,7 +97,7 @@ export class SsoComponent extends BaseSsoComponent {
}
// Fallback to state svc if domain is unclaimed
const storedIdentifier = await this.stateService.getSsoOrgIdentifier();
const storedIdentifier = await this.ssoLoginService.getOrganizationSsoIdentifier();
if (storedIdentifier != null) {
this.identifier = storedIdentifier;
}
@ -118,7 +121,7 @@ export class SsoComponent extends BaseSsoComponent {
}
async submit() {
await this.stateService.setSsoOrganizationIdentifier(this.identifier);
await this.ssoLoginService.setOrganizationSsoIdentifier(this.identifier);
if (this.clientId === "browser") {
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
}

View File

@ -7,6 +7,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
@ -43,6 +44,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window,
) {
@ -60,6 +62,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
twoFactorService,
appIdService,
loginService,
ssoLoginService,
configService,
);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;

View File

@ -5,6 +5,7 @@ import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -28,6 +29,7 @@ export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
}
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
apiService: ApiService,
@ -42,6 +44,7 @@ export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
configService: ConfigServiceAbstraction,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,

View File

@ -21,6 +21,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -88,6 +89,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction,
) {}
async ngOnInit() {
@ -163,7 +165,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
async loadNewUserData() {
const autoEnrollStatus$ = defer(() =>
this.stateService.getUserSsoOrganizationIdentifier(),
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
).pipe(
switchMap((organizationIdentifier) => {
if (organizationIdentifier == undefined) {

View File

@ -7,6 +7,7 @@ import { take, takeUntil } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -78,6 +79,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected formValidationErrorService: FormValidationErrorsService,
protected route: ActivatedRoute,
protected loginService: LoginService,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
) {
super(environmentService, i18nService, platformUtilsService);
@ -241,8 +243,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
// Save sso params
await this.stateService.setSsoState(state);
await this.stateService.setSsoCodeVerifier(ssoCodeVerifier);
await this.ssoLoginService.setSsoState(state);
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
// Build URI
const webUrl = this.environmentService.getWebVaultUrl();

View File

@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
@ -63,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
super(
@ -96,7 +98,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
} 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.stateService.getUserSsoOrganizationIdentifier();
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier();
}
}),
filter((orgSsoId) => orgSsoId != null),

View File

@ -6,6 +6,7 @@ import { Observable, of } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -53,6 +54,7 @@ describe("SsoComponent", () => {
let mockQueryParams: Observable<any>;
let mockActivatedRoute: ActivatedRoute;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockStateService: MockProxy<StateService>;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
let mockApiService: MockProxy<ApiService>;
@ -99,6 +101,7 @@ describe("SsoComponent", () => {
queryParams: mockQueryParams,
} as any as ActivatedRoute;
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockStateService = mock<StateService>();
mockPlatformUtilsService = mock<PlatformUtilsService>();
mockApiService = mock<ApiService>();
@ -167,6 +170,7 @@ describe("SsoComponent", () => {
TestBed.configureTestingModule({
declarations: [TestSsoComponent],
providers: [
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: LoginStrategyServiceAbstraction, useValue: mockLoginStrategyService },
{ provide: Router, useValue: mockRouter },
{ provide: I18nService, useValue: mockI18nService },

View File

@ -4,6 +4,7 @@ import { first } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
@ -46,6 +47,7 @@ export class SsoComponent {
protected codeChallenge: string;
constructor(
protected ssoLoginService: SsoLoginServiceAbstraction,
protected loginStrategyService: LoginStrategyServiceAbstraction,
protected router: Router,
protected i18nService: I18nService,
@ -64,10 +66,10 @@ export class SsoComponent {
// eslint-disable-next-line rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.code != null && qParams.state != null) {
const codeVerifier = await this.stateService.getSsoCodeVerifier();
const state = await this.stateService.getSsoState();
await this.stateService.setSsoCodeVerifier(null);
await this.stateService.setSsoState(null);
const codeVerifier = await this.ssoLoginService.getCodeVerifier();
const state = await this.ssoLoginService.getSsoState();
await this.ssoLoginService.setCodeVerifier(null);
await this.ssoLoginService.setSsoState(null);
if (
qParams.code != null &&
codeVerifier != null &&
@ -133,7 +135,7 @@ export class SsoComponent {
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.stateService.setSsoCodeVerifier(codeVerifier);
await this.ssoLoginService.setCodeVerifier(codeVerifier);
}
if (state == null) {
@ -147,7 +149,7 @@ export class SsoComponent {
state += `_identifier=${this.identifier}`;
// Save state (regardless of new or existing)
await this.stateService.setSsoState(state);
await this.ssoLoginService.setSsoState(state);
let authorizeUrl =
this.environmentService.getIdentityUrl() +
@ -203,7 +205,7 @@ export class SsoComponent {
// - 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.stateService.setUserSsoOrganizationIdentifier(orgSsoIdentifier);
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
// 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)

View File

@ -8,6 +8,7 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -55,6 +56,7 @@ describe("TwoFactorComponent", () => {
let mockTwoFactorService: MockProxy<TwoFactorService>;
let mockAppIdService: MockProxy<AppIdService>;
let mockLoginService: MockProxy<LoginService>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
let mockAcctDecryptionOpts: {
@ -81,6 +83,7 @@ describe("TwoFactorComponent", () => {
mockTwoFactorService = mock<TwoFactorService>();
mockAppIdService = mock<AppIdService>();
mockLoginService = mock<LoginService>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigServiceAbstraction>();
mockAcctDecryptionOpts = {
@ -150,6 +153,7 @@ describe("TwoFactorComponent", () => {
{ provide: TwoFactorService, useValue: mockTwoFactorService },
{ provide: AppIdService, useValue: mockAppIdService },
{ provide: LoginService, useValue: mockLoginService },
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
],
});

View File

@ -8,6 +8,7 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
@ -83,6 +84,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService,
protected loginService: LoginService,
protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigServiceAbstraction,
) {
super(environmentService, i18nService, platformUtilsService);
@ -278,7 +280,7 @@ 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.stateService.setUserSsoOrganizationIdentifier(this.orgIdentifier);
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
this.loginService.clearValues();
// note: this flow affects both TDE & standard users

View File

@ -55,6 +55,7 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
@ -73,6 +74,7 @@ import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
@ -534,6 +536,11 @@ import { ModalService } from "./modal.service";
provide: VaultTimeoutServiceAbstraction,
useExisting: VaultTimeoutService,
},
{
provide: SsoLoginServiceAbstraction,
useClass: SsoLoginService,
deps: [StateProvider],
},
{
provide: StateServiceAbstraction,
useClass: StateService,

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");