mirror of
https://github.com/bitwarden/clients.git
synced 2026-02-06 17:13:40 +08:00
* [EC-1070] Introduce flag for enforcing master password policy on login * [EC-1070] Update master password policy form Add the ability to toggle enforceOnLogin flag in web * [EC-1070] Add API method to retrieve all policies for the current user * [EC-1070] Refactor forcePasswordReset in state service to support more options - Use an options class to provide a reason and optional organization id - Use the OnDiskMemory storage location so the option persists between the same auth session * [AC-1070] Retrieve single master password policy from identity token response Additionally, store the policy in the login strategy for future use * [EC-1070] Introduce master password evaluation in the password login strategy - If a master password policy is returned from the identity result, evaluate the password. - If the password does not meet the requirements, save the forcePasswordReset options - Add support for 2FA by storing the results of the password evaluation on the login strategy instance - Add unit tests to password login strategy * [AC-1070] Modify admin password reset component to support update master password on login - Modify the warning message to depend on the reason - Use the forcePasswordResetOptions in the update temp password component * [EC-1070] Require current master password when updating weak mp on login - Inject user verification service to verify the user - Conditionally show the current master password field only when updating a weak mp. Admin reset does not require the current master password. * [EC-1070] Implement password policy check during vault unlock Checking the master password during unlock is the only applicable place to enforce the master password policy check for SSO users. * [EC-1070] CLI - Add ability to load MP policies on login Inject policyApi and organization services into the login command * [EC-1070] CLI - Refactor update temp password logic to support updating weak passwords - Introduce new shared method for collecting a valid and confirmed master password from the CLI and generating a new encryption key - Add separate methods for updating temp passwords and weak passwords. - Utilize those methods during login flow if not using an API key * [EC-1070] Add route guard to force password reset when required * [AC-1070] Use master password policy from verify password response in lock component * [EC-1070] Update labels in update password component * [AC-1070] Fix policy service tests * [AC-1070] CLI - Force sync before any password reset flow Move up the call to sync the vault before attempting to collect a new master password. Ensures the master password policies are available. * [AC-1070] Remove unused getAllPolicies method from policy api service * [AC-1070] Fix missing enforceOnLogin copy in policy service * [AC-1070] Include current master password on desktop/browser update password page templates * [AC-1070] Check for forced password reset on account switch in Desktop * [AC-1070] Rename WeakMasterPasswordOnLogin to WeakMasterPassword * [AC-1070] Update AuthServiceInitOptions * [AC-1070] Add None force reset password reason * [AC-1070] Remove redundant ForcePasswordResetOptions class and replace with ForcePasswordResetReason enum * [AC-1070] Rename ForceResetPasswordReason file * [AC-1070] Simplify conditional * [AC-1070] Refactor logic that saves password reset flag * [AC-1070] Remove redundant constructors * [AC-1070] Remove unnecessary state service call * [AC-1070] Update master password policy component - Use typed reactive form - Use CL form components - Remove bootstrap - Update error component to support min/max - Use Utils.minimumPasswordLength value for min value form validation * [AC-1070] Cleanup leftover html comment * [AC-1070] Remove overridden default values from MasterPasswordPolicyResponse * [AC-1070] Hide current master password input in browser for admin password reset * [AC-1070] Remove clientside user verification * [AC-1070] Update temp password web component to use CL - Use CL for form inputs in the Web component template - Remove most of the bootstrap classes in the Web component template - Use userVerificationService to build the password request - Remove redundant current master password null check * [AC-1070] Replace repeated user inputs email parsing helpers - Update passwordStrength() method to accept an optional email argument that will be parsed into separate user inputs for use with zxcvbn - Remove all other repeated getUserInput helper methods that parsed user emails and use the new passwordStrength signature * [AC-1070] Fix broken login command after forcePasswordReset enum refactor * [AC-1070] Reduce side effects in base login strategy - Remove masterPasswordPolicy property from base login.strategy.ts - Include an IdentityResponse in base startLogin() in addition to AuthResult - Use the new IdentityResponse to parse the master password policy info only in the PasswordLoginStrategy * [AC-1070] Cleanup password login strategy tests * [AC-1070] Remove unused field * [AC-1070] Strongly type postAccountVerifyPassword API service method - Remove redundant verify master password response - Use MasterPasswordPolicyResponse instead * [AC-1070] Use ForceResetPassword.None during account switch check * [AC-1070] Fix check for forcePasswordReset reason after addition of None * [AC-1070] Redirect a user home if on the update temp password page without a reason * [AC-1070] Use bit-select and bit-option * [AC-1070] Reduce explicit form control definitions for readability * [AC-1070] Import SelectModule in Shared web module * [AC-1070] Add check for missing 'at' symbol * [AC-1070] Remove redundant unpacking and null coalescing * [AC-1070] Update passwordStrength signature and add jsdocs * [AC-1070] Remove variable abbreviation * [AC-1070] Restore Id attributes on form inputs * [AC-1070] Clarify input value min/max error messages * [AC-1070] Add input min/max value example to storybook * [AC-1070] Add missing spinner to update temp password form * [AC-1070] Add missing ids to form elements * [AC-1070] Remove duplicate force sync and update comment * [AC-1070] Switch backticks to quotation marks --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
263 lines
9.1 KiB
TypeScript
263 lines
9.1 KiB
TypeScript
import { Directive } from "@angular/core";
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
import { first } from "rxjs/operators";
|
|
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
|
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
|
import { SsoLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
|
|
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
|
import { Utils } from "@bitwarden/common/misc/utils";
|
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
|
|
|
@Directive()
|
|
export class SsoComponent {
|
|
identifier: string;
|
|
loggingIn = false;
|
|
|
|
formPromise: Promise<AuthResult>;
|
|
initiateSsoFormPromise: Promise<SsoPreValidateResponse>;
|
|
onSuccessfulLogin: () => Promise<any>;
|
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
|
onSuccessfulLoginChangePasswordNavigate: () => Promise<any>;
|
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
|
|
|
protected twoFactorRoute = "2fa";
|
|
protected successRoute = "lock";
|
|
protected changePasswordRoute = "set-password";
|
|
protected forcePasswordResetRoute = "update-temp-password";
|
|
protected clientId: string;
|
|
protected redirectUri: string;
|
|
protected state: string;
|
|
protected codeChallenge: string;
|
|
|
|
constructor(
|
|
protected authService: AuthService,
|
|
protected router: Router,
|
|
protected i18nService: I18nService,
|
|
protected route: ActivatedRoute,
|
|
protected stateService: StateService,
|
|
protected platformUtilsService: PlatformUtilsService,
|
|
protected apiService: ApiService,
|
|
protected cryptoFunctionService: CryptoFunctionService,
|
|
protected environmentService: EnvironmentService,
|
|
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
|
protected logService: LogService
|
|
) {}
|
|
|
|
async ngOnInit() {
|
|
// eslint-disable-next-line rxjs/no-async-subscribe
|
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
|
if (qParams.code != null && qParams.state != null) {
|
|
const codeVerifier = await this.stateService.getSsoCodeVerifier();
|
|
const state = await this.stateService.getSsoState();
|
|
await this.stateService.setSsoCodeVerifier(null);
|
|
await this.stateService.setSsoState(null);
|
|
if (
|
|
qParams.code != null &&
|
|
codeVerifier != null &&
|
|
state != null &&
|
|
this.checkState(state, qParams.state)
|
|
) {
|
|
await this.logIn(
|
|
qParams.code,
|
|
codeVerifier,
|
|
this.getOrgIdentifierFromState(qParams.state)
|
|
);
|
|
}
|
|
} else if (
|
|
qParams.clientId != null &&
|
|
qParams.redirectUri != null &&
|
|
qParams.state != null &&
|
|
qParams.codeChallenge != null
|
|
) {
|
|
this.redirectUri = qParams.redirectUri;
|
|
this.state = qParams.state;
|
|
this.codeChallenge = qParams.codeChallenge;
|
|
this.clientId = qParams.clientId;
|
|
}
|
|
});
|
|
}
|
|
|
|
async submit(returnUri?: string, includeUserIdentifier?: boolean) {
|
|
if (this.identifier == null || this.identifier === "") {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("ssoValidationFailed"),
|
|
this.i18nService.t("ssoIdentifierRequired")
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.initiateSsoFormPromise = this.apiService.preValidateSso(this.identifier);
|
|
const response = await this.initiateSsoFormPromise;
|
|
|
|
const authorizeUrl = await this.buildAuthorizeUrl(
|
|
returnUri,
|
|
includeUserIdentifier,
|
|
response.token
|
|
);
|
|
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
|
|
}
|
|
|
|
protected async buildAuthorizeUrl(
|
|
returnUri?: string,
|
|
includeUserIdentifier?: boolean,
|
|
token?: string
|
|
): Promise<string> {
|
|
let codeChallenge = this.codeChallenge;
|
|
let state = this.state;
|
|
|
|
const passwordOptions: any = {
|
|
type: "password",
|
|
length: 64,
|
|
uppercase: true,
|
|
lowercase: true,
|
|
numbers: true,
|
|
special: false,
|
|
};
|
|
|
|
if (codeChallenge == null) {
|
|
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
|
codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
|
await this.stateService.setSsoCodeVerifier(codeVerifier);
|
|
}
|
|
|
|
if (state == null) {
|
|
state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
if (returnUri) {
|
|
state += `_returnUri='${returnUri}'`;
|
|
}
|
|
}
|
|
|
|
// Add Organization Identifier to state
|
|
state += `_identifier=${this.identifier}`;
|
|
|
|
// Save state (regardless of new or existing)
|
|
await this.stateService.setSsoState(state);
|
|
|
|
let authorizeUrl =
|
|
this.environmentService.getIdentityUrl() +
|
|
"/connect/authorize?" +
|
|
"client_id=" +
|
|
this.clientId +
|
|
"&redirect_uri=" +
|
|
encodeURIComponent(this.redirectUri) +
|
|
"&" +
|
|
"response_type=code&scope=api offline_access&" +
|
|
"state=" +
|
|
state +
|
|
"&code_challenge=" +
|
|
codeChallenge +
|
|
"&" +
|
|
"code_challenge_method=S256&response_mode=query&" +
|
|
"domain_hint=" +
|
|
encodeURIComponent(this.identifier) +
|
|
"&ssoToken=" +
|
|
encodeURIComponent(token);
|
|
|
|
if (includeUserIdentifier) {
|
|
const userIdentifier = await this.apiService.getSsoUserIdentifier();
|
|
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
|
|
}
|
|
|
|
return authorizeUrl;
|
|
}
|
|
|
|
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
|
this.loggingIn = true;
|
|
try {
|
|
const credentials = new SsoLogInCredentials(
|
|
code,
|
|
codeVerifier,
|
|
this.redirectUri,
|
|
orgIdFromState
|
|
);
|
|
this.formPromise = this.authService.logIn(credentials);
|
|
const response = await this.formPromise;
|
|
if (response.requiresTwoFactor) {
|
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
|
this.onSuccessfulLoginTwoFactorNavigate();
|
|
} else {
|
|
this.router.navigate([this.twoFactorRoute], {
|
|
queryParams: {
|
|
identifier: orgIdFromState,
|
|
sso: "true",
|
|
},
|
|
});
|
|
}
|
|
} else if (response.resetMasterPassword) {
|
|
if (this.onSuccessfulLoginChangePasswordNavigate != null) {
|
|
this.onSuccessfulLoginChangePasswordNavigate();
|
|
} else {
|
|
this.router.navigate([this.changePasswordRoute], {
|
|
queryParams: {
|
|
identifier: orgIdFromState,
|
|
},
|
|
});
|
|
}
|
|
} else if (response.forcePasswordReset !== ForceResetPasswordReason.None) {
|
|
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
|
this.onSuccessfulLoginForceResetNavigate();
|
|
} else {
|
|
this.router.navigate([this.forcePasswordResetRoute]);
|
|
}
|
|
} else {
|
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
|
if (this.onSuccessfulLogin != null) {
|
|
this.onSuccessfulLogin();
|
|
}
|
|
if (this.onSuccessfulLoginNavigate != null) {
|
|
this.onSuccessfulLoginNavigate();
|
|
} else {
|
|
this.router.navigate([this.successRoute]);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
|
|
// TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here
|
|
if (e.message === "Key Connector error") {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
null,
|
|
this.i18nService.t("ssoKeyConnectorError")
|
|
);
|
|
}
|
|
}
|
|
this.loggingIn = false;
|
|
}
|
|
|
|
private getOrgIdentifierFromState(state: string): string {
|
|
if (state === null || state === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const stateSplit = state.split("_identifier=");
|
|
return stateSplit.length > 1 ? stateSplit[1] : null;
|
|
}
|
|
|
|
private checkState(state: string, checkState: string): boolean {
|
|
if (state === null || state === undefined) {
|
|
return false;
|
|
}
|
|
if (checkState === null || checkState === undefined) {
|
|
return false;
|
|
}
|
|
|
|
const stateSplit = state.split("_identifier=");
|
|
const checkStateSplit = checkState.split("_identifier=");
|
|
return stateSplit[0] === checkStateSplit[0];
|
|
}
|
|
}
|