diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index 3177cfdd596..448cbad8942 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -104,9 +104,9 @@ export class WebLoginComponentService if ( await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) ) { - // Properly error if we don't have an org invite with + // Properly error if we don't have an org invite enforcedPasswordPolicyOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(orgInvite.userId, policies), + this.policyService.masterPasswordPolicyOptionsPriorToSync$(policies), ); } else { enforcedPasswordPolicyOptions = await firstValueFrom( diff --git a/apps/web/src/app/auth/organization-invite/organization-invite.ts b/apps/web/src/app/auth/organization-invite/organization-invite.ts index d52d5e41d02..65414113e74 100644 --- a/apps/web/src/app/auth/organization-invite/organization-invite.ts +++ b/apps/web/src/app/auth/organization-invite/organization-invite.ts @@ -3,8 +3,6 @@ import { Params } from "@angular/router"; import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - export class OrganizationInvite { email: string; initOrganization: boolean; @@ -14,7 +12,6 @@ export class OrganizationInvite { organizationName: string; organizationUserId: string; token: string; - userId: UserId; static fromJSON(json: Jsonify): OrganizationInvite | null { if (json == null) { @@ -38,7 +35,6 @@ export class OrganizationInvite { organizationName: params.organizationName, organizationUserId: params.organizationUserId, token: params.token, - userId: params.userId, }); } } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 2b1ed1cff78..e08497fa243 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -85,6 +85,8 @@ export class LoginComponent implements OnInit, OnDestroy { isKnownDevice = false; loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY; + passwordPoliciesFromOrgInvite?: Policy[]; + formGroup = this.formBuilder.group( { email: ["", [Validators.required, Validators.email]], @@ -238,8 +240,15 @@ export class LoginComponent implements OnInit, OnDestroy { this.loginComponentService.getOrgPoliciesFromOrgInvite ) { const orgPoliciesFromInvite = await this.loginComponentService.getOrgPoliciesFromOrgInvite(); - const orgPolicies = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions ?? undefined; - credentials = new PasswordLoginCredentials(email, masterPassword, undefined, orgPolicies); + const orgMasterPasswordPolicyOptions = + orgPoliciesFromInvite?.enforcedPasswordPolicyOptions ?? undefined; + this.passwordPoliciesFromOrgInvite = orgPoliciesFromInvite?.policies; + credentials = new PasswordLoginCredentials( + email, + masterPassword, + undefined, + orgMasterPasswordPolicyOptions, + ); } else { credentials = new PasswordLoginCredentials(email, masterPassword); } @@ -329,8 +338,13 @@ export class LoginComponent implements OnInit, OnDestroy { // The AuthGuard will handle routing to update-temp-password based on state if ( - !(await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)) + await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) ) { + // Check if we had a + if (this.passwordPoliciesFromOrgInvite) { + await this.setPoliciesIntoState(authResult.userId, this.passwordPoliciesFromOrgInvite); + } + } else { // TODO: PM-18269 - evaluate if we can combine this with the // password evaluation done in the password login strategy. // If there's an existing org invite, use it to get the org's password policies diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 1c97650f111..d67d482f5be 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -98,6 +98,8 @@ export class PasswordLoginStrategy extends LoginStrategy { await this.buildDeviceRequest(), ); + // TODO: add master password policy conditions to the cache so that it is available after 2fa for password evaluation + this.cache.next(data); const [authResult, identityResponse] = await this.startLogIn(); diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index bf02872ed7c..1d478c2249d 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -44,12 +44,37 @@ export abstract class PolicyService { * @param policies The policies to be evaluated; if null or undefined, it will default to using policies from sync data. * @returns a set of options which represent the minimum Master Password settings that the user must * comply with in order to comply with **all** applicable Master Password policies. + * + * @deprecated Deprecating because the parameters can be made more strict and clear. */ abstract masterPasswordPolicyOptions$: ( userId: UserId, policies?: Policy[], ) => Observable; + /** + * Combines all Master Password policies that apply to the user. + * Used for after a login / sync has occurred and the policy state has been set in state. + * @param userId The user against whom the policy needs to be enforced. + * @returns a set of options which represent the minimum Master Password settings that the user must + * comply with in order to comply with **all** applicable Master Password policies. + */ + abstract masterPasswordPolicyOptionsByUserId$: ( + userId: UserId, + ) => Observable; + + /** + * Combines all Master Password policies that apply to the user that comes from the policies that + * are passed to this function. + * This would be used prior to obtaining a user id, such as before login for an org invite. + * @param policies The policies to be evaluated; if null or undefined, it will default to using policies from sync data. + * @returns a set of options which represent the minimum Master Password settings that the user must + * comply with in order to comply with **all** applicable Master Password policies. + */ + abstract masterPasswordPolicyOptionsPriorToSync$: ( + policies: Policy[], + ) => Observable; + /** * Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user. */ diff --git a/libs/common/src/admin-console/services/policy/default-policy.service.ts b/libs/common/src/admin-console/services/policy/default-policy.service.ts index 1158d29d737..aac58a27298 100644 --- a/libs/common/src/admin-console/services/policy/default-policy.service.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.ts @@ -87,63 +87,19 @@ export class DefaultPolicyService implements PolicyService { policies?: Policy[], ): Observable { const policies$ = policies ? of(policies) : this.policies$(userId); - return policies$.pipe( - map((obsPolicies) => { - let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined; - const filteredPolicies = - obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? []; + return policies$.pipe(map((obsPolicies) => this.policyMapping(obsPolicies))); + } - if (filteredPolicies.length === 0) { - return; - } + masterPasswordPolicyOptionsByUserId$( + userId: UserId, + ): Observable { + return this.policies$(userId).pipe(map((obsPolicies) => this.policyMapping(obsPolicies))); + } - filteredPolicies.forEach((currentPolicy) => { - if (!currentPolicy.enabled || !currentPolicy.data) { - return; - } - - if (!enforcedOptions) { - enforcedOptions = new MasterPasswordPolicyOptions(); - } - - if ( - currentPolicy.data.minComplexity != null && - currentPolicy.data.minComplexity > enforcedOptions.minComplexity - ) { - enforcedOptions.minComplexity = currentPolicy.data.minComplexity; - } - - if ( - currentPolicy.data.minLength != null && - currentPolicy.data.minLength > enforcedOptions.minLength - ) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.requireUpper) { - enforcedOptions.requireUpper = true; - } - - if (currentPolicy.data.requireLower) { - enforcedOptions.requireLower = true; - } - - if (currentPolicy.data.requireNumbers) { - enforcedOptions.requireNumbers = true; - } - - if (currentPolicy.data.requireSpecial) { - enforcedOptions.requireSpecial = true; - } - - if (currentPolicy.data.enforceOnLogin) { - enforcedOptions.enforceOnLogin = true; - } - }); - - return enforcedOptions; - }), - ); + masterPasswordPolicyOptionsPriorToSync$( + policies: Policy[], + ): Observable { + return of(policies).pipe(map((obsPolicies) => this.policyMapping(obsPolicies))); } evaluateMasterPassword( @@ -241,4 +197,59 @@ export class DefaultPolicyService implements PolicyService { return organization.canManagePolicies; } } + + private policyMapping(obsPolicies: Policy[]): MasterPasswordPolicyOptions | undefined { + let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined; + const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? []; + + if (filteredPolicies.length === 0) { + return; + } + + filteredPolicies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || !currentPolicy.data) { + return; + } + + if (!enforcedOptions) { + enforcedOptions = new MasterPasswordPolicyOptions(); + } + + if ( + currentPolicy.data.minComplexity != null && + currentPolicy.data.minComplexity > enforcedOptions.minComplexity + ) { + enforcedOptions.minComplexity = currentPolicy.data.minComplexity; + } + + if ( + currentPolicy.data.minLength != null && + currentPolicy.data.minLength > enforcedOptions.minLength + ) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.requireUpper) { + enforcedOptions.requireUpper = true; + } + + if (currentPolicy.data.requireLower) { + enforcedOptions.requireLower = true; + } + + if (currentPolicy.data.requireNumbers) { + enforcedOptions.requireNumbers = true; + } + + if (currentPolicy.data.requireSpecial) { + enforcedOptions.requireSpecial = true; + } + + if (currentPolicy.data.enforceOnLogin) { + enforcedOptions.enforceOnLogin = true; + } + }); + + return enforcedOptions; + } }