[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": {
|
"loginOrCreateNewAccount": {
|
||||||
"message": "Log in or create a new account to access your secure vault."
|
"message": "Log in or create a new account to access your secure vault."
|
||||||
},
|
},
|
||||||
|
"inviteAccepted": {
|
||||||
|
"message": "Invitation accepted"
|
||||||
|
},
|
||||||
"createAccount": {
|
"createAccount": {
|
||||||
"message": "Create account"
|
"message": "Create account"
|
||||||
},
|
},
|
||||||
|
@ -68,6 +71,12 @@
|
||||||
"masterPassHint": {
|
"masterPassHint": {
|
||||||
"message": "Master password hint (optional)"
|
"message": "Master password hint (optional)"
|
||||||
},
|
},
|
||||||
|
"joinOrganization": {
|
||||||
|
"message": "Join organization"
|
||||||
|
},
|
||||||
|
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||||
|
"message": "Finish joining this organization by setting a master password."
|
||||||
|
},
|
||||||
"tab": {
|
"tab": {
|
||||||
"message": "Tab"
|
"message": "Tab"
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
RegistrationStartComponent,
|
RegistrationStartComponent,
|
||||||
RegistrationStartSecondaryComponent,
|
RegistrationStartSecondaryComponent,
|
||||||
RegistrationStartSecondaryComponentData,
|
RegistrationStartSecondaryComponentData,
|
||||||
|
SetPasswordJitComponent,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
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,
|
RegistrationStartComponent,
|
||||||
RegistrationStartSecondaryComponent,
|
RegistrationStartSecondaryComponent,
|
||||||
RegistrationStartSecondaryComponentData,
|
RegistrationStartSecondaryComponentData,
|
||||||
|
SetPasswordJitComponent,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
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,
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
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 { 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 { 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 { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.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 { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
|
|
||||||
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||||
import { ElectronCryptoService } from "../../platform/services/electron-crypto.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 { SearchBarService } from "../layout/search/search-bar.service";
|
||||||
|
|
||||||
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
||||||
|
import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
|
||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
||||||
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
||||||
|
@ -254,6 +268,20 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: CLIENT_TYPE,
|
provide: CLIENT_TYPE,
|
||||||
useValue: ClientType.Desktop,
|
useValue: ClientType.Desktop,
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetPasswordJitService,
|
||||||
|
useClass: DesktopSetPasswordJitService,
|
||||||
|
deps: [
|
||||||
|
ApiService,
|
||||||
|
CryptoService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigService,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -551,6 +551,12 @@
|
||||||
"masterPassHintLabel": {
|
"masterPassHintLabel": {
|
||||||
"message": "Master password hint"
|
"message": "Master password hint"
|
||||||
},
|
},
|
||||||
|
"joinOrganization": {
|
||||||
|
"message": "Join organization"
|
||||||
|
},
|
||||||
|
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||||
|
"message": "Finish joining this organization by setting a master password."
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"message": "Settings"
|
"message": "Settings"
|
||||||
},
|
},
|
||||||
|
@ -2093,6 +2099,9 @@
|
||||||
"vaultTimeoutTooLarge": {
|
"vaultTimeoutTooLarge": {
|
||||||
"message": "Your vault timeout exceeds the restrictions set by your organization."
|
"message": "Your vault timeout exceeds the restrictions set by your organization."
|
||||||
},
|
},
|
||||||
|
"inviteAccepted": {
|
||||||
|
"message": "Invitation accepted"
|
||||||
|
},
|
||||||
"resetPasswordPolicyAutoEnroll": {
|
"resetPasswordPolicyAutoEnroll": {
|
||||||
"message": "Automatic enrollment"
|
"message": "Automatic enrollment"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./webauthn-login";
|
export * from "./webauthn-login";
|
||||||
|
export * from "./set-password-jit";
|
||||||
export * from "./registration";
|
export * from "./registration";
|
||||||
|
|
|
@ -145,6 +145,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||||
passwordInputResult = {
|
passwordInputResult = {
|
||||||
masterKey: masterKey,
|
masterKey: masterKey,
|
||||||
masterKeyHash: "masterKeyHash",
|
masterKeyHash: "masterKeyHash",
|
||||||
|
localMasterKeyHash: "localMasterKeyHash",
|
||||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||||
hint: "hint",
|
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";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
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 { 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { ClientType } from "@bitwarden/common/enums";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||||
|
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
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 { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
|
@ -184,6 +193,20 @@ const safeProviders: SafeProvider[] = [
|
||||||
PolicyService,
|
PolicyService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetPasswordJitService,
|
||||||
|
useClass: WebSetPasswordJitService,
|
||||||
|
deps: [
|
||||||
|
ApiService,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigService,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
RegistrationStartComponent,
|
RegistrationStartComponent,
|
||||||
RegistrationStartSecondaryComponent,
|
RegistrationStartSecondaryComponent,
|
||||||
RegistrationStartSecondaryComponentData,
|
RegistrationStartSecondaryComponentData,
|
||||||
|
SetPasswordJitComponent,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
RegistrationLinkExpiredComponent,
|
RegistrationLinkExpiredComponent,
|
||||||
} from "@bitwarden/auth/angular";
|
} 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",
|
path: "signup-link-expired",
|
||||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||||
|
|
|
@ -3471,6 +3471,9 @@
|
||||||
"joinOrganizationDesc": {
|
"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."
|
"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": {
|
"inviteAccepted": {
|
||||||
"message": "Invitation accepted"
|
"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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
@ -319,6 +320,14 @@ export class SsoComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||||
|
const emailVerification = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EmailVerification,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailVerification) {
|
||||||
|
this.changePasswordRoute = "set-password-jit";
|
||||||
|
}
|
||||||
|
|
||||||
await this.navigateViaCallbackOrRoute(
|
await this.navigateViaCallbackOrRoute(
|
||||||
this.onSuccessfulLoginChangePasswordNavigate,
|
this.onSuccessfulLoginChangePasswordNavigate,
|
||||||
[this.changePasswordRoute],
|
[this.changePasswordRoute],
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
SetPasswordJitService,
|
||||||
|
DefaultSetPasswordJitService,
|
||||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||||
DefaultRegistrationFinishService,
|
DefaultRegistrationFinishService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
|
@ -1265,6 +1267,20 @@ const safeProviders: SafeProvider[] = [
|
||||||
useClass: StripeService,
|
useClass: StripeService,
|
||||||
deps: [LogService],
|
deps: [LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SetPasswordJitService,
|
||||||
|
useClass: DefaultSetPasswordJitService,
|
||||||
|
deps: [
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
KdfConfigServiceAbstraction,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationUserService,
|
||||||
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: RegisterRouteService,
|
provide: RegisterRouteService,
|
||||||
useClass: RegisterRouteService,
|
useClass: RegisterRouteService,
|
||||||
|
|
|
@ -5,13 +5,26 @@
|
||||||
// icons
|
// icons
|
||||||
export * from "./icons";
|
export * from "./icons";
|
||||||
|
|
||||||
|
// anon layout
|
||||||
export * from "./anon-layout/anon-layout.component";
|
export * from "./anon-layout/anon-layout.component";
|
||||||
export * from "./anon-layout/anon-layout-wrapper.component";
|
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||||
|
|
||||||
|
// fingerprint dialog
|
||||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
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 "./password-callout/password-callout.component";
|
||||||
export * from "./vault-timeout-input/vault-timeout-input.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
|
// user verification
|
||||||
export * from "./user-verification/user-verification-dialog.component";
|
export * from "./user-verification/user-verification-dialog.component";
|
||||||
export * from "./user-verification/user-verification-dialog.types";
|
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-env-selector/registration-env-selector.component";
|
||||||
export * from "./registration/registration-finish/registration-finish.service";
|
export * from "./registration/registration-finish/registration-finish.service";
|
||||||
export * from "./registration/registration-finish/default-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>
|
<bit-hint>
|
||||||
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||||
{{ "masterPassImportant" | i18n }}
|
{{ "masterPassImportant" | i18n }}
|
||||||
{{ minPasswordMsg }}.
|
{{ minPasswordLengthMsg }}.
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</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 { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
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 { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
|
@ -48,17 +49,16 @@ import { PasswordInputResult } from "./password-input-result";
|
||||||
JslibModule,
|
JslibModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class InputPasswordComponent implements OnInit {
|
export class InputPasswordComponent {
|
||||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||||
|
|
||||||
@Input({ required: true }) email: string;
|
@Input({ required: true }) email: string;
|
||||||
@Input() protected buttonText: string;
|
@Input() buttonText: string;
|
||||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||||
@Input() loading: boolean = false;
|
@Input() loading: boolean = false;
|
||||||
|
|
||||||
private minHintLength = 0;
|
private minHintLength = 0;
|
||||||
protected maxHintLength = 50;
|
protected maxHintLength = 50;
|
||||||
|
|
||||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||||
protected minPasswordMsg = "";
|
protected minPasswordMsg = "";
|
||||||
protected passwordStrengthScore: PasswordStrengthScore;
|
protected passwordStrengthScore: PasswordStrengthScore;
|
||||||
|
@ -103,17 +103,14 @@ export class InputPasswordComponent implements OnInit {
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
get minPasswordLengthMsg() {
|
||||||
if (
|
if (
|
||||||
this.masterPasswordPolicyOptions != null &&
|
this.masterPasswordPolicyOptions != null &&
|
||||||
this.masterPasswordPolicyOptions.minLength > 0
|
this.masterPasswordPolicyOptions.minLength > 0
|
||||||
) {
|
) {
|
||||||
this.minPasswordMsg = this.i18nService.t(
|
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
|
||||||
"characterMinimum",
|
|
||||||
this.masterPasswordPolicyOptions.minLength,
|
|
||||||
);
|
|
||||||
} else {
|
} 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 masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
|
||||||
|
|
||||||
|
const localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||||
|
password,
|
||||||
|
masterKey,
|
||||||
|
HashPurpose.LocalAuthorization,
|
||||||
|
);
|
||||||
|
|
||||||
this.onPasswordFormSubmit.emit({
|
this.onPasswordFormSubmit.emit({
|
||||||
masterKey,
|
masterKey,
|
||||||
masterKeyHash,
|
masterKeyHash,
|
||||||
|
localMasterKeyHash,
|
||||||
kdfConfig,
|
kdfConfig,
|
||||||
hint: this.formGroup.controls.hint.value,
|
hint: this.formGroup.controls.hint.value,
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
export interface PasswordInputResult {
|
export interface PasswordInputResult {
|
||||||
masterKey: MasterKey;
|
masterKey: MasterKey;
|
||||||
masterKeyHash: string;
|
masterKeyHash: string;
|
||||||
|
localMasterKeyHash: string;
|
||||||
kdfConfig: PBKDF2KdfConfig;
|
kdfConfig: PBKDF2KdfConfig;
|
||||||
hint: string;
|
hint: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<bit-callout>
|
<bit-callout>
|
||||||
{{ message | i18n }}
|
{{ 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">
|
<li *ngIf="policy?.minComplexity > 0">
|
||||||
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -54,6 +54,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||||
passwordInputResult = {
|
passwordInputResult = {
|
||||||
masterKey: masterKey,
|
masterKey: masterKey,
|
||||||
masterKeyHash: "masterKeyHash",
|
masterKeyHash: "masterKeyHash",
|
||||||
|
localMasterKeyHash: "localMasterKeyHash",
|
||||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||||
hint: "hint",
|
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