Implement password policy validation

This commit is contained in:
Joseph Garrone 2024-04-22 06:34:50 +02:00
parent cf42f5b2a0
commit 83548afa0a
2 changed files with 211 additions and 16 deletions

View File

@ -660,21 +660,17 @@ export declare namespace Validators {
export type PasswordPolicies = {
/** The minimum length of the password */
length?: number;
length?: `${number}`;
/** The minimum number of digits required in the password */
digits?: number;
digits?: `${number}`;
/** The minimum number of lowercase characters required in the password */
lowerCase?: number;
lowerCase?: `${number}`;
/** The minimum number of uppercase characters required in the password */
upperCase?: number;
upperCase?: `${number}`;
/** The minimum number of special characters required in the password */
specialChars?: number;
/** Whether the password can contain the username */
specialChars?: `${number}`;
/** Whether the password can be the username */
notUsername?: boolean;
/** Whether the password can contain the email address */
/** Whether the password can be the email address */
notEmail?: boolean;
/** The number of previous passwords that cannot be reused */
passwordHistory?: number;
/** The number of days before the password expires */
forceExpiredPasswordChange?: number;
};

View File

@ -58,7 +58,6 @@ export type KcContextLike = {
export type ParamsOfUseUserProfileForm = {
kcContext: KcContextLike;
passwordValidators?: Validators;
i18n: I18n;
passwordConfirmationDisabled?: boolean;
};
@ -74,7 +73,7 @@ export type ReturnTypeOfUseUserProfileForm = {
* artificial password related attributes only if kcContext.passwordRequired === true
*/
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
const { kcContext, passwordValidators = {}, i18n, passwordConfirmationDisabled = false } = params;
const { kcContext, i18n, passwordConfirmationDisabled = false } = params;
const attributesWithPassword = useMemo(() => {
const attributesWithPassword: Attribute[] = [];
@ -95,7 +94,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
"displayName": id<`\${${MessageKey}}`>("${password}"),
"required": true,
"readOnly": false,
"validators": passwordValidators,
"validators": {},
"annotations": {},
"autocomplete": "new-password",
"html5DataAnnotations": {},
@ -282,7 +281,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
/** Expect to be used in a component wrapped within a <I18nProvider> */
function useGetErrors(params: {
kcContext: Pick<KcContextLike, "messagesPerField">;
kcContext: Pick<KcContextLike, "messagesPerField" | "passwordPolicies">;
attributes: {
name: string;
validators: Validators;
@ -294,7 +293,7 @@ function useGetErrors(params: {
}) {
const { kcContext, attributes, i18n } = params;
const { messagesPerField } = kcContext;
const { messagesPerField, passwordPolicies } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
@ -346,6 +345,206 @@ function useGetErrors(params: {
const errors: FormFieldError[] = [];
check_password_policies: {
if (name !== "password") {
break check_password_policies;
}
if (passwordPolicies === undefined) {
break check_password_policies;
}
check_password_policy_x: {
const policyName = "length";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minLength = parseInt(policy);
assert(!isNaN(minLength));
if (value.length >= minLength) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "digits";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfDigits = parseInt(policy);
assert(!isNaN(minNumberOfDigits));
if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "lowerCase";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfLowerCaseChar = parseInt(policy);
assert(!isNaN(minNumberOfLowerCaseChar));
if (
value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "upperCase";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfUpperCaseChar = parseInt(policy);
assert(!isNaN(minNumberOfUpperCaseChar));
if (
value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "specialChars";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfSpecialChar = parseInt(policy);
assert(!isNaN(minNumberOfSpecialChar));
if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "notUsername";
const notUsername = passwordPolicies[policyName];
if (!notUsername) {
break check_password_policy_x;
}
const usernameFieldValue = fieldValues.find(fieldValue => fieldValue.name === "username");
if (usernameFieldValue === undefined) {
break check_password_policy_x;
}
if (value !== usernameFieldValue.value) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
check_password_policy_x: {
const policyName = "notEmail";
const notEmail = passwordPolicies[policyName];
if (!notEmail) {
break check_password_policy_x;
}
const emailFieldValue = fieldValues.find(fieldValue => fieldValue.name === "email");
if (emailFieldValue === undefined) {
break check_password_policy_x;
}
if (value !== emailFieldValue.value) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
errors.push({
"validatorName": undefined,
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs)
});
}
}
password_confirm_matches_password: {
if (name !== "password-confirm") {
break password_confirm_matches_password;