[PM-7330] Create SetPasswordComponent (email verification) (#9810)
* setup SetPassword component * accept query params * add InputPasswordComponent to template * add route * add dynamic translation with org name * feature flag route * setup onInit * add set password logic * move to libs * remove comments * update AuthGuard routing * use ToastService * replace deprecated methods * replace orgId input with policy input * use getter for msg instead of ngOnInit * cleanup * refactor to use services * more refactoring of service * address browser routing and translations * add desktop service * simplify queryParam handler * remove ngOnDestroy * small edits * use inject() * add jsdocs * create basic tests * add success toasts on successfuly set password * add tests * update feature-flag * move model to service * refactor client services to override setPassword() * add error handling to setPassword() * move auto enroll logic to service * update tests * fix test * adjust padding on password-callout list * revert refactor of auto enroll logic * refactor keyPair generation to own method * update page title and button text * update pageSubtitle and translations * fix test
This commit is contained in:
parent
c4c949c15a
commit
9355a9bb43
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
|
|
|
@ -145,6 +145,7 @@ describe("DefaultRegistrationFinishService", () => {
|
|||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
};
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./web-set-password-jit.service";
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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()],
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<bit-hint>
|
||||
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||
{{ "masterPassImportant" | i18n }}
|
||||
{{ minPasswordMsg }}.
|
||||
{{ minPasswordLengthMsg }}.
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
|
|
|
@ -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<PasswordInputResult>();
|
||||
|
||||
@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,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
|||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<bit-callout>
|
||||
{{ message | i18n }}
|
||||
|
||||
<ul *ngIf="policy" class="tw-mb-0">
|
||||
<ul *ngIf="policy" class="tw-mb-0 tw-ml-8 tw-ps-0">
|
||||
<li *ngIf="policy?.minComplexity > 0">
|
||||
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
||||
</li>
|
||||
|
|
|
@ -54,6 +54,7 @@ describe("DefaultRegistrationFinishService", () => {
|
|||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
};
|
||||
|
|
|
@ -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<ApiService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserService = mock<OrganizationUserService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
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<UserDecryptionOptions>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<void> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<ng-container *ngIf="syncLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-mr-2" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!syncLoading">
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="resetPasswordAutoEnroll"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[email]="email"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
|
@ -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"]);
|
||||
}
|
||||
}
|
|
@ -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<void>;
|
||||
}
|
Loading…
Reference in New Issue