diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 4bd656a282..a29a42a07a 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -13,6 +13,9 @@
"loginOrCreateNewAccount": {
"message": "Log in or create a new account to access your secure vault."
},
+ "inviteAccepted": {
+ "message": "Invitation accepted"
+ },
"createAccount": {
"message": "Create account"
},
@@ -68,6 +71,12 @@
"masterPassHint": {
"message": "Master password hint (optional)"
},
+ "joinOrganization": {
+ "message": "Join organization"
+ },
+ "finishJoiningThisOrganizationBySettingAMasterPassword": {
+ "message": "Finish joining this organization by setting a master password."
+ },
"tab": {
"message": "Tab"
},
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index bb9d038378..57195564c7 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -16,6 +16,7 @@ import {
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
+ SetPasswordJitComponent,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -409,6 +410,15 @@ const routes: Routes = [
},
],
},
+ {
+ path: "set-password-jit",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
+ component: SetPasswordJitComponent,
+ data: {
+ pageTitle: "joinOrganization",
+ pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
+ } satisfies AnonLayoutWrapperData,
+ },
],
},
{
diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts
index 0e3f10345a..a93299e45a 100644
--- a/apps/desktop/src/app/app-routing.module.ts
+++ b/apps/desktop/src/app/app-routing.module.ts
@@ -16,6 +16,7 @@ import {
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
+ SetPasswordJitComponent,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -149,6 +150,15 @@ const routes: Routes = [
},
],
},
+ {
+ path: "set-password-jit",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
+ component: SetPasswordJitComponent,
+ data: {
+ pageTitle: "joinOrganization",
+ pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
+ } satisfies AnonLayoutWrapperData,
+ },
],
},
];
diff --git a/apps/desktop/src/app/services/desktop-set-password-jit.service.ts b/apps/desktop/src/app/services/desktop-set-password-jit.service.ts
new file mode 100644
index 0000000000..f6ea3d0ce8
--- /dev/null
+++ b/apps/desktop/src/app/services/desktop-set-password-jit.service.ts
@@ -0,0 +1,21 @@
+import { inject } from "@angular/core";
+
+import {
+ DefaultSetPasswordJitService,
+ SetPasswordCredentials,
+ SetPasswordJitService,
+} from "@bitwarden/auth/angular";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+
+export class DesktopSetPasswordJitService
+ extends DefaultSetPasswordJitService
+ implements SetPasswordJitService
+{
+ messagingService = inject(MessagingService);
+
+ override async setPassword(credentials: SetPasswordCredentials) {
+ await super.setPassword(credentials);
+
+ this.messagingService.send("redrawMenu");
+ }
+}
diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts
index c0b4bf4eb1..85bfbc09f6 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -18,16 +18,30 @@ import {
CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
+import { SetPasswordJitService } from "@bitwarden/auth/angular";
+import {
+ InternalUserDecryptionOptionsServiceAbstraction,
+ PinServiceAbstraction,
+} from "@bitwarden/auth/common";
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.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 { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
-import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
+import {
+ KdfConfigService,
+ KdfConfigService as KdfConfigServiceAbstraction,
+} from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
-import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
+import {
+ CryptoService,
+ CryptoService as CryptoServiceAbstraction,
+} from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -56,7 +70,6 @@ import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vau
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
-import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
@@ -77,6 +90,7 @@ import { NativeMessagingService } from "../../services/native-messaging.service"
import { SearchBarService } from "../layout/search/search-bar.service";
import { DesktopFileDownloadService } from "./desktop-file-download.service";
+import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
import { InitService } from "./init.service";
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
@@ -254,6 +268,20 @@ const safeProviders: SafeProvider[] = [
provide: CLIENT_TYPE,
useValue: ClientType.Desktop,
}),
+ safeProvider({
+ provide: SetPasswordJitService,
+ useClass: DesktopSetPasswordJitService,
+ deps: [
+ ApiService,
+ CryptoService,
+ I18nServiceAbstraction,
+ KdfConfigService,
+ InternalMasterPasswordServiceAbstraction,
+ OrganizationApiServiceAbstraction,
+ OrganizationUserService,
+ InternalUserDecryptionOptionsServiceAbstraction,
+ ],
+ }),
];
@NgModule({
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 7b7dc28b31..868367cf18 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -551,6 +551,12 @@
"masterPassHintLabel": {
"message": "Master password hint"
},
+ "joinOrganization": {
+ "message": "Join organization"
+ },
+ "finishJoiningThisOrganizationBySettingAMasterPassword": {
+ "message": "Finish joining this organization by setting a master password."
+ },
"settings": {
"message": "Settings"
},
@@ -2093,6 +2099,9 @@
"vaultTimeoutTooLarge": {
"message": "Your vault timeout exceeds the restrictions set by your organization."
},
+ "inviteAccepted": {
+ "message": "Invitation accepted"
+ },
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic enrollment"
},
diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts
index 6a28efcbaa..c85f0f3204 100644
--- a/apps/web/src/app/auth/core/services/index.ts
+++ b/apps/web/src/app/auth/core/services/index.ts
@@ -1,2 +1,3 @@
export * from "./webauthn-login";
+export * from "./set-password-jit";
export * from "./registration";
diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts
index 999e603ef6..007165a1bc 100644
--- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts
+++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts
@@ -145,6 +145,7 @@ describe("DefaultRegistrationFinishService", () => {
passwordInputResult = {
masterKey: masterKey,
masterKeyHash: "masterKeyHash",
+ localMasterKeyHash: "localMasterKeyHash",
kdfConfig: DEFAULT_KDF_CONFIG,
hint: "hint",
};
diff --git a/apps/web/src/app/auth/core/services/set-password-jit/index.ts b/apps/web/src/app/auth/core/services/set-password-jit/index.ts
new file mode 100644
index 0000000000..fc119fd964
--- /dev/null
+++ b/apps/web/src/app/auth/core/services/set-password-jit/index.ts
@@ -0,0 +1 @@
+export * from "./web-set-password-jit.service";
diff --git a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts
new file mode 100644
index 0000000000..62175f1256
--- /dev/null
+++ b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts
@@ -0,0 +1,27 @@
+import { inject } from "@angular/core";
+
+import {
+ DefaultSetPasswordJitService,
+ SetPasswordCredentials,
+ SetPasswordJitService,
+} from "@bitwarden/auth/angular";
+
+import { RouterService } from "../../../../core/router.service";
+import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
+
+export class WebSetPasswordJitService
+ extends DefaultSetPasswordJitService
+ implements SetPasswordJitService
+{
+ routerService = inject(RouterService);
+ acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
+
+ override async setPassword(credentials: SetPasswordCredentials) {
+ await super.setPassword(credentials);
+
+ // SSO JIT accepts org invites when setting their MP, meaning
+ // we can clear the deep linked url for accepting it.
+ await this.routerService.getAndClearLoginRedirectUrl();
+ await this.acceptOrganizationInviteService.clearOrganizationInvitation();
+ }
+}
diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts
index 93a5a9c00a..45cfa8a355 100644
--- a/apps/web/src/app/core/core.module.ts
+++ b/apps/web/src/app/core/core.module.ts
@@ -17,11 +17,20 @@ import {
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
-import { RegistrationFinishService as RegistrationFinishServiceAbstraction } from "@bitwarden/auth/angular";
+import {
+ SetPasswordJitService,
+ RegistrationFinishService as RegistrationFinishServiceAbstraction,
+} from "@bitwarden/auth/angular";
+import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
+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 { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
+import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -48,7 +57,7 @@ import {
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { PolicyListService } from "../admin-console/core/policy-list.service";
-import { WebRegistrationFinishService } from "../auth";
+import { WebSetPasswordJitService, WebRegistrationFinishService } from "../auth";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
@@ -184,6 +193,20 @@ const safeProviders: SafeProvider[] = [
PolicyService,
],
}),
+ safeProvider({
+ provide: SetPasswordJitService,
+ useClass: WebSetPasswordJitService,
+ deps: [
+ ApiService,
+ CryptoServiceAbstraction,
+ I18nServiceAbstraction,
+ KdfConfigService,
+ InternalMasterPasswordServiceAbstraction,
+ OrganizationApiServiceAbstraction,
+ OrganizationUserService,
+ InternalUserDecryptionOptionsServiceAbstraction,
+ ],
+ }),
];
@NgModule({
diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts
index a4172ad03e..6b382756f9 100644
--- a/apps/web/src/app/oss-routing.module.ts
+++ b/apps/web/src/app/oss-routing.module.ts
@@ -17,6 +17,7 @@ import {
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
+ SetPasswordJitComponent,
LockIcon,
RegistrationLinkExpiredComponent,
} from "@bitwarden/auth/angular";
@@ -206,6 +207,15 @@ const routes: Routes = [
},
],
},
+ {
+ path: "set-password-jit",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
+ component: SetPasswordJitComponent,
+ data: {
+ pageTitle: "joinOrganization",
+ pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
+ } satisfies AnonLayoutWrapperData,
+ },
{
path: "signup-link-expired",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 6c16d8569f..04121c7894 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -3471,6 +3471,9 @@
"joinOrganizationDesc": {
"message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account."
},
+ "finishJoiningThisOrganizationBySettingAMasterPassword": {
+ "message": "Finish joining this organization by setting a master password."
+ },
"inviteAccepted": {
"message": "Invitation accepted"
},
diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts
index 2c6f7b29cb..42119be360 100644
--- a/libs/angular/src/auth/components/sso.component.ts
+++ b/libs/angular/src/auth/components/sso.component.ts
@@ -17,6 +17,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -319,6 +320,14 @@ export class SsoComponent {
}
private async handleChangePasswordRequired(orgIdentifier: string) {
+ const emailVerification = await this.configService.getFeatureFlag(
+ FeatureFlag.EmailVerification,
+ );
+
+ if (emailVerification) {
+ this.changePasswordRoute = "set-password-jit";
+ }
+
await this.navigateViaCallbackOrRoute(
this.onSuccessfulLoginChangePasswordNavigate,
[this.changePasswordRoute],
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 56b2e956cd..3294e59751 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -2,6 +2,8 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
import { Subject } from "rxjs";
import {
+ SetPasswordJitService,
+ DefaultSetPasswordJitService,
RegistrationFinishService as RegistrationFinishServiceAbstraction,
DefaultRegistrationFinishService,
} from "@bitwarden/auth/angular";
@@ -1265,6 +1267,20 @@ const safeProviders: SafeProvider[] = [
useClass: StripeService,
deps: [LogService],
}),
+ safeProvider({
+ provide: SetPasswordJitService,
+ useClass: DefaultSetPasswordJitService,
+ deps: [
+ ApiServiceAbstraction,
+ CryptoServiceAbstraction,
+ I18nServiceAbstraction,
+ KdfConfigServiceAbstraction,
+ InternalMasterPasswordServiceAbstraction,
+ OrganizationApiServiceAbstraction,
+ OrganizationUserService,
+ InternalUserDecryptionOptionsServiceAbstraction,
+ ],
+ }),
safeProvider({
provide: RegisterRouteService,
useClass: RegisterRouteService,
diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts
index b594477243..f455f42eef 100644
--- a/libs/auth/src/angular/index.ts
+++ b/libs/auth/src/angular/index.ts
@@ -5,13 +5,26 @@
// icons
export * from "./icons";
+// anon layout
export * from "./anon-layout/anon-layout.component";
export * from "./anon-layout/anon-layout-wrapper.component";
+
+// fingerprint dialog
export * from "./fingerprint-dialog/fingerprint-dialog.component";
-export * from "./input-password/input-password.component";
+
+// password callout
export * from "./password-callout/password-callout.component";
export * from "./vault-timeout-input/vault-timeout-input.component";
+// input password
+export * from "./input-password/input-password.component";
+export * from "./input-password/password-input-result";
+
+// set password (JIT user)
+export * from "./set-password-jit/set-password-jit.component";
+export * from "./set-password-jit/set-password-jit.service.abstraction";
+export * from "./set-password-jit/default-set-password-jit.service";
+
// user verification
export * from "./user-verification/user-verification-dialog.component";
export * from "./user-verification/user-verification-dialog.types";
@@ -25,6 +38,3 @@ export * from "./registration/registration-start/registration-start-secondary.co
export * from "./registration/registration-env-selector/registration-env-selector.component";
export * from "./registration/registration-finish/registration-finish.service";
export * from "./registration/registration-finish/default-registration-finish.service";
-
-// input password
-export * from "./input-password/password-input-result";
diff --git a/libs/auth/src/angular/input-password/input-password.component.html b/libs/auth/src/angular/input-password/input-password.component.html
index 42d67a77c9..54c832fbd6 100644
--- a/libs/auth/src/angular/input-password/input-password.component.html
+++ b/libs/auth/src/angular/input-password/input-password.component.html
@@ -23,7 +23,7 @@
{{ "important" | i18n }}
{{ "masterPassImportant" | i18n }}
- {{ minPasswordMsg }}.
+ {{ minPasswordLengthMsg }}.
diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts
index 7b5651492e..0fa5d0ac41 100644
--- a/libs/auth/src/angular/input-password/input-password.component.ts
+++ b/libs/auth/src/angular/input-password/input-password.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
+import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -12,6 +12,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
@@ -48,17 +49,16 @@ import { PasswordInputResult } from "./password-input-result";
JslibModule,
],
})
-export class InputPasswordComponent implements OnInit {
+export class InputPasswordComponent {
@Output() onPasswordFormSubmit = new EventEmitter();
@Input({ required: true }) email: string;
- @Input() protected buttonText: string;
+ @Input() buttonText: string;
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
@Input() loading: boolean = false;
private minHintLength = 0;
protected maxHintLength = 50;
-
protected minPasswordLength = Utils.minimumPasswordLength;
protected minPasswordMsg = "";
protected passwordStrengthScore: PasswordStrengthScore;
@@ -103,17 +103,14 @@ export class InputPasswordComponent implements OnInit {
private toastService: ToastService,
) {}
- async ngOnInit() {
+ get minPasswordLengthMsg() {
if (
this.masterPasswordPolicyOptions != null &&
this.masterPasswordPolicyOptions.minLength > 0
) {
- this.minPasswordMsg = this.i18nService.t(
- "characterMinimum",
- this.masterPasswordPolicyOptions.minLength,
- );
+ return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
} else {
- this.minPasswordMsg = this.i18nService.t("characterMinimum", this.minPasswordLength);
+ return this.i18nService.t("characterMinimum", this.minPasswordLength);
}
}
@@ -181,9 +178,16 @@ export class InputPasswordComponent implements OnInit {
const masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
+ const localMasterKeyHash = await this.cryptoService.hashMasterKey(
+ password,
+ masterKey,
+ HashPurpose.LocalAuthorization,
+ );
+
this.onPasswordFormSubmit.emit({
masterKey,
masterKeyHash,
+ localMasterKeyHash,
kdfConfig,
hint: this.formGroup.controls.hint.value,
});
diff --git a/libs/auth/src/angular/input-password/password-input-result.ts b/libs/auth/src/angular/input-password/password-input-result.ts
index 0a74b88a2e..ce9f0b7386 100644
--- a/libs/auth/src/angular/input-password/password-input-result.ts
+++ b/libs/auth/src/angular/input-password/password-input-result.ts
@@ -4,6 +4,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
export interface PasswordInputResult {
masterKey: MasterKey;
masterKeyHash: string;
+ localMasterKeyHash: string;
kdfConfig: PBKDF2KdfConfig;
hint: string;
}
diff --git a/libs/auth/src/angular/password-callout/password-callout.component.html b/libs/auth/src/angular/password-callout/password-callout.component.html
index f8a73ab623..589d2a2045 100644
--- a/libs/auth/src/angular/password-callout/password-callout.component.html
+++ b/libs/auth/src/angular/password-callout/password-callout.component.html
@@ -1,7 +1,7 @@
{{ message | i18n }}
-
+
- 0">
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts
index 94eccfce2f..b4c8026a5e 100644
--- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts
+++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts
@@ -54,6 +54,7 @@ describe("DefaultRegistrationFinishService", () => {
passwordInputResult = {
masterKey: masterKey,
masterKeyHash: "masterKeyHash",
+ localMasterKeyHash: "localMasterKeyHash",
kdfConfig: DEFAULT_KDF_CONFIG,
hint: "hint",
};
diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts
new file mode 100644
index 0000000000..e4837641ef
--- /dev/null
+++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts
@@ -0,0 +1,230 @@
+import { MockProxy, mock } from "jest-mock-extended";
+import { BehaviorSubject, of } from "rxjs";
+
+import {
+ FakeUserDecryptionOptions as UserDecryptionOptions,
+ InternalUserDecryptionOptionsServiceAbstraction,
+} from "@bitwarden/auth/common";
+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 { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
+import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
+import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
+import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
+import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
+import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
+import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import { CsprngArray } from "@bitwarden/common/types/csprng";
+import { UserId } from "@bitwarden/common/types/guid";
+import { MasterKey, UserKey } from "@bitwarden/common/types/key";
+
+import { PasswordInputResult } from "../input-password/password-input-result";
+
+import { DefaultSetPasswordJitService } from "./default-set-password-jit.service";
+import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
+
+describe("DefaultSetPasswordJitService", () => {
+ let sut: DefaultSetPasswordJitService;
+
+ let apiService: MockProxy;
+ let cryptoService: MockProxy;
+ let i18nService: MockProxy;
+ let kdfConfigService: MockProxy;
+ let masterPasswordService: MockProxy;
+ let organizationApiService: MockProxy;
+ let organizationUserService: MockProxy;
+ let userDecryptionOptionsService: MockProxy;
+
+ beforeEach(() => {
+ apiService = mock();
+ cryptoService = mock();
+ i18nService = mock();
+ kdfConfigService = mock();
+ masterPasswordService = mock();
+ organizationApiService = mock();
+ organizationUserService = mock();
+ userDecryptionOptionsService = mock();
+
+ sut = new DefaultSetPasswordJitService(
+ apiService,
+ cryptoService,
+ i18nService,
+ kdfConfigService,
+ masterPasswordService,
+ organizationApiService,
+ organizationUserService,
+ userDecryptionOptionsService,
+ );
+ });
+
+ it("should instantiate the DefaultSetPasswordJitService", () => {
+ expect(sut).not.toBeFalsy();
+ });
+
+ describe("setPassword", () => {
+ let masterKey: MasterKey;
+ let userKey: UserKey;
+ let userKeyEncString: EncString;
+ let protectedUserKey: [UserKey, EncString];
+ let keyPair: [string, EncString];
+ let keysRequest: KeysRequest;
+ let organizationKeys: OrganizationKeysResponse;
+ let orgPublicKey: Uint8Array;
+
+ let orgSsoIdentifier: string;
+ let orgId: string;
+ let resetPasswordAutoEnroll: boolean;
+ let userId: UserId;
+ let passwordInputResult: PasswordInputResult;
+ let credentials: SetPasswordCredentials;
+
+ let userDecryptionOptionsSubject: BehaviorSubject;
+ let setPasswordRequest: SetPasswordRequest;
+
+ beforeEach(() => {
+ masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
+ userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
+ userKeyEncString = new EncString("userKeyEncrypted");
+ protectedUserKey = [userKey, userKeyEncString];
+ keyPair = ["publicKey", new EncString("privateKey")];
+ keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
+ organizationKeys = {
+ privateKey: "orgPrivateKey",
+ publicKey: "orgPublicKey",
+ } as OrganizationKeysResponse;
+ orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
+
+ orgSsoIdentifier = "orgSsoIdentifier";
+ orgId = "orgId";
+ resetPasswordAutoEnroll = false;
+ userId = "userId" as UserId;
+
+ passwordInputResult = {
+ masterKey: masterKey,
+ masterKeyHash: "masterKeyHash",
+ localMasterKeyHash: "localMasterKeyHash",
+ hint: "hint",
+ kdfConfig: DEFAULT_KDF_CONFIG,
+ };
+
+ credentials = {
+ ...passwordInputResult,
+ orgSsoIdentifier,
+ orgId,
+ resetPasswordAutoEnroll,
+ userId,
+ };
+
+ userDecryptionOptionsSubject = new BehaviorSubject(null);
+ userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
+
+ setPasswordRequest = new SetPasswordRequest(
+ passwordInputResult.masterKeyHash,
+ protectedUserKey[1].encryptedString,
+ passwordInputResult.hint,
+ orgSsoIdentifier,
+ keysRequest,
+ passwordInputResult.kdfConfig.kdfType,
+ passwordInputResult.kdfConfig.iterations,
+ );
+ });
+
+ function setupSetPasswordMocks(hasUserKey = true) {
+ if (!hasUserKey) {
+ cryptoService.userKey$.mockReturnValue(of(null));
+ cryptoService.makeUserKey.mockResolvedValue(protectedUserKey);
+ } else {
+ cryptoService.userKey$.mockReturnValue(of(userKey));
+ cryptoService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey);
+ }
+
+ cryptoService.makeKeyPair.mockResolvedValue(keyPair);
+
+ apiService.setPassword.mockResolvedValue(undefined);
+ masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
+
+ userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
+ userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined);
+ kdfConfigService.setKdfConfig.mockResolvedValue(undefined);
+ cryptoService.setUserKey.mockResolvedValue(undefined);
+
+ cryptoService.setPrivateKey.mockResolvedValue(undefined);
+
+ masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined);
+ }
+
+ function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) {
+ if (organizationKeysExist) {
+ organizationApiService.getKeys.mockResolvedValue(organizationKeys);
+ } else {
+ organizationApiService.getKeys.mockResolvedValue(null);
+ return;
+ }
+
+ cryptoService.userKey$.mockReturnValue(of(userKey));
+ cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString);
+
+ organizationUserService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
+ undefined,
+ );
+ }
+
+ it("should set password successfully (given a user key)", async () => {
+ // Arrange
+ setupSetPasswordMocks();
+
+ // Act
+ await sut.setPassword(credentials);
+
+ // Assert
+ expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
+ });
+
+ it("should set password successfully (given no user key)", async () => {
+ // Arrange
+ setupSetPasswordMocks(false);
+
+ // Act
+ await sut.setPassword(credentials);
+
+ // Assert
+ expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
+ });
+
+ it("should handle reset password auto enroll", async () => {
+ // Arrange
+ credentials.resetPasswordAutoEnroll = true;
+
+ setupSetPasswordMocks();
+ setupResetPasswordAutoEnrollMocks();
+
+ // Act
+ await sut.setPassword(credentials);
+
+ // Assert
+ expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
+ expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
+ expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
+ expect(organizationUserService.putOrganizationUserResetPasswordEnrollment).toHaveBeenCalled();
+ });
+
+ it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
+ // Arrange
+ credentials.resetPasswordAutoEnroll = true;
+
+ setupSetPasswordMocks();
+ setupResetPasswordAutoEnrollMocks(false);
+
+ // Act and Assert
+ await expect(sut.setPassword(credentials)).rejects.toThrow();
+ expect(
+ organizationUserService.putOrganizationUserResetPasswordEnrollment,
+ ).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts
new file mode 100644
index 0000000000..a5c196b5c7
--- /dev/null
+++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts
@@ -0,0 +1,170 @@
+import { firstValueFrom } from "rxjs";
+
+import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
+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 { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
+import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
+import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
+import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
+import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
+import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
+import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
+import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
+import { UserId } from "@bitwarden/common/types/guid";
+import { MasterKey, UserKey } from "@bitwarden/common/types/key";
+
+import {
+ SetPasswordCredentials,
+ SetPasswordJitService,
+} from "./set-password-jit.service.abstraction";
+
+export class DefaultSetPasswordJitService implements SetPasswordJitService {
+ constructor(
+ protected apiService: ApiService,
+ protected cryptoService: CryptoService,
+ protected i18nService: I18nService,
+ protected kdfConfigService: KdfConfigService,
+ protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
+ protected organizationApiService: OrganizationApiServiceAbstraction,
+ protected organizationUserService: OrganizationUserService,
+ protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
+ ) {}
+
+ async setPassword(credentials: SetPasswordCredentials): Promise {
+ const {
+ masterKey,
+ masterKeyHash,
+ localMasterKeyHash,
+ hint,
+ kdfConfig,
+ orgSsoIdentifier,
+ orgId,
+ resetPasswordAutoEnroll,
+ userId,
+ } = credentials;
+
+ for (const [key, value] of Object.entries(credentials)) {
+ if (value == null) {
+ throw new Error(`${key} not found. Could not set password.`);
+ }
+ }
+
+ const protectedUserKey = await this.makeProtectedUserKey(masterKey, userId);
+ if (protectedUserKey == null) {
+ throw new Error("protectedUserKey not found. Could not set password.");
+ }
+
+ // Since this is an existing JIT provisioned user in a MP encryption org setting first password,
+ // they will not already have a user asymmetric key pair so we must create it for them.
+ const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
+
+ const request = new SetPasswordRequest(
+ masterKeyHash,
+ protectedUserKey[1].encryptedString,
+ hint,
+ orgSsoIdentifier,
+ keysRequest,
+ kdfConfig.kdfType, // kdfConfig is always DEFAULT_KDF_CONFIG (see InputPasswordComponent)
+ kdfConfig.iterations,
+ );
+
+ await this.apiService.setPassword(request);
+
+ // Clear force set password reason to allow navigation back to vault.
+ await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
+
+ // User now has a password so update account decryption options in state
+ await this.updateAccountDecryptionProperties(masterKey, kdfConfig, protectedUserKey, userId);
+
+ await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, userId);
+
+ await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
+
+ if (resetPasswordAutoEnroll) {
+ await this.handleResetPasswordAutoEnroll(masterKeyHash, orgId, userId);
+ }
+ }
+
+ private async makeProtectedUserKey(
+ masterKey: MasterKey,
+ userId: UserId,
+ ): Promise<[UserKey, EncString]> {
+ let protectedUserKey: [UserKey, EncString] = null;
+
+ const userKey = await firstValueFrom(this.cryptoService.userKey$(userId));
+
+ if (userKey == null) {
+ protectedUserKey = await this.cryptoService.makeUserKey(masterKey);
+ } else {
+ protectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey);
+ }
+
+ return protectedUserKey;
+ }
+
+ private async makeKeyPairAndRequest(
+ protectedUserKey: [UserKey, EncString],
+ ): Promise<[[string, EncString], KeysRequest]> {
+ const keyPair = await this.cryptoService.makeKeyPair(protectedUserKey[0]);
+ if (keyPair == null) {
+ throw new Error("keyPair not found. Could not set password.");
+ }
+ const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
+
+ return [keyPair, keysRequest];
+ }
+
+ private async updateAccountDecryptionProperties(
+ masterKey: MasterKey,
+ kdfConfig: PBKDF2KdfConfig,
+ protectedUserKey: [UserKey, EncString],
+ userId: UserId,
+ ) {
+ const userDecryptionOpts = await firstValueFrom(
+ this.userDecryptionOptionsService.userDecryptionOptions$,
+ );
+ userDecryptionOpts.hasMasterPassword = true;
+ await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
+ await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
+ await this.masterPasswordService.setMasterKey(masterKey, userId);
+ await this.cryptoService.setUserKey(protectedUserKey[0], userId);
+ }
+
+ private async handleResetPasswordAutoEnroll(
+ masterKeyHash: string,
+ orgId: string,
+ userId: UserId,
+ ) {
+ const organizationKeys = await this.organizationApiService.getKeys(orgId);
+
+ if (organizationKeys == null) {
+ throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
+ }
+
+ const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
+
+ // RSA Encrypt user key with organization public key
+ const userKey = await firstValueFrom(this.cryptoService.userKey$(userId));
+
+ if (userKey == null) {
+ throw new Error("userKey not found. Could not handle reset password auto enroll.");
+ }
+
+ const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
+
+ const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
+ resetRequest.masterPasswordHash = masterKeyHash;
+ resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
+
+ await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
+ orgId,
+ userId,
+ resetRequest,
+ );
+ }
+}
diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.html b/libs/auth/src/angular/set-password-jit/set-password-jit.component.html
new file mode 100644
index 0000000000..aa6a122993
--- /dev/null
+++ b/libs/auth/src/angular/set-password-jit/set-password-jit.component.html
@@ -0,0 +1,22 @@
+
+
+ {{ "loading" | i18n }}
+
+
+
+
+ {{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
+
+
+
+
diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts
new file mode 100644
index 0000000000..80b0adc2bc
--- /dev/null
+++ b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts
@@ -0,0 +1,126 @@
+import { CommonModule } from "@angular/common";
+import { Component, OnInit } from "@angular/core";
+import { ActivatedRoute, Router } from "@angular/router";
+import { firstValueFrom, map } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
+import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
+import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
+import { UserId } from "@bitwarden/common/types/guid";
+import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
+
+import { ToastService } from "../../../../components/src/toast";
+import { InputPasswordComponent } from "../input-password/input-password.component";
+import { PasswordInputResult } from "../input-password/password-input-result";
+
+import {
+ SetPasswordCredentials,
+ SetPasswordJitService,
+} from "./set-password-jit.service.abstraction";
+
+@Component({
+ standalone: true,
+ selector: "auth-set-password-jit",
+ templateUrl: "set-password-jit.component.html",
+ imports: [CommonModule, InputPasswordComponent, JslibModule],
+})
+export class SetPasswordJitComponent implements OnInit {
+ protected email: string;
+ protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
+ protected orgId: string;
+ protected orgSsoIdentifier: string;
+ protected resetPasswordAutoEnroll: boolean;
+ protected submitting = false;
+ protected syncLoading = true;
+ protected userId: UserId;
+
+ constructor(
+ private accountService: AccountService,
+ private activatedRoute: ActivatedRoute,
+ private i18nService: I18nService,
+ private organizationApiService: OrganizationApiServiceAbstraction,
+ private policyApiService: PolicyApiServiceAbstraction,
+ private router: Router,
+ private setPasswordJitService: SetPasswordJitService,
+ private syncService: SyncService,
+ private toastService: ToastService,
+ private validationService: ValidationService,
+ ) {}
+
+ async ngOnInit() {
+ this.email = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.email)),
+ );
+
+ await this.syncService.fullSync(true);
+ this.syncLoading = false;
+
+ await this.handleQueryParams();
+ }
+
+ private async handleQueryParams() {
+ const qParams = await firstValueFrom(this.activatedRoute.queryParams);
+
+ if (qParams.identifier != null) {
+ try {
+ this.orgSsoIdentifier = qParams.identifier;
+
+ const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
+ this.orgSsoIdentifier,
+ );
+ this.orgId = autoEnrollStatus.id;
+ this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
+ this.masterPasswordPolicyOptions =
+ await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id);
+ } catch {
+ this.toastService.showToast({
+ variant: "error",
+ title: null,
+ message: this.i18nService.t("errorOccurred"),
+ });
+ }
+ }
+ }
+
+ protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
+ this.submitting = true;
+
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+
+ const credentials: SetPasswordCredentials = {
+ ...passwordInputResult,
+ orgSsoIdentifier: this.orgSsoIdentifier,
+ orgId: this.orgId,
+ resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
+ userId,
+ };
+
+ try {
+ await this.setPasswordJitService.setPassword(credentials);
+ } catch (e) {
+ this.validationService.showError(e);
+ this.submitting = false;
+ return;
+ }
+
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("accountSuccessfullyCreated"),
+ });
+
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("inviteAccepted"),
+ });
+
+ this.submitting = false;
+
+ await this.router.navigate(["vault"]);
+ }
+}
diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts
new file mode 100644
index 0000000000..165b4a6180
--- /dev/null
+++ b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts
@@ -0,0 +1,33 @@
+import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
+import { UserId } from "@bitwarden/common/types/guid";
+import { MasterKey } from "@bitwarden/common/types/key";
+
+export interface SetPasswordCredentials {
+ masterKey: MasterKey;
+ masterKeyHash: string;
+ localMasterKeyHash: string;
+ kdfConfig: PBKDF2KdfConfig;
+ hint: string;
+ orgSsoIdentifier: string;
+ orgId: string;
+ resetPasswordAutoEnroll: boolean;
+ userId: UserId;
+}
+
+/**
+ * This service handles setting a password for a "just-in-time" provisioned user.
+ *
+ * A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the
+ * time they first click "Login with SSO". Once they click "Login with SSO" we register the account on
+ * the fly ("just-in-time").
+ */
+export abstract class SetPasswordJitService {
+ /**
+ * Sets the password for a JIT provisioned user.
+ *
+ * @param credentials An object of the credentials needed to set the password for a JIT provisioned user
+ * @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
+ * or newKeyPair could not be created.
+ */
+ setPassword: (credentials: SetPasswordCredentials) => Promise;
+}