Implement password policy validation
This commit is contained in:
parent
cf42f5b2a0
commit
83548afa0a
@ -660,21 +660,17 @@ export declare namespace Validators {
|
|||||||
|
|
||||||
export type PasswordPolicies = {
|
export type PasswordPolicies = {
|
||||||
/** The minimum length of the password */
|
/** The minimum length of the password */
|
||||||
length?: number;
|
length?: `${number}`;
|
||||||
/** The minimum number of digits required in the password */
|
/** The minimum number of digits required in the password */
|
||||||
digits?: number;
|
digits?: `${number}`;
|
||||||
/** The minimum number of lowercase characters required in the password */
|
/** The minimum number of lowercase characters required in the password */
|
||||||
lowerCase?: number;
|
lowerCase?: `${number}`;
|
||||||
/** The minimum number of uppercase characters required in the password */
|
/** The minimum number of uppercase characters required in the password */
|
||||||
upperCase?: number;
|
upperCase?: `${number}`;
|
||||||
/** The minimum number of special characters required in the password */
|
/** The minimum number of special characters required in the password */
|
||||||
specialChars?: number;
|
specialChars?: `${number}`;
|
||||||
/** Whether the password can contain the username */
|
/** Whether the password can be the username */
|
||||||
notUsername?: boolean;
|
notUsername?: boolean;
|
||||||
/** Whether the password can contain the email address */
|
/** Whether the password can be the email address */
|
||||||
notEmail?: boolean;
|
notEmail?: boolean;
|
||||||
/** The number of previous passwords that cannot be reused */
|
|
||||||
passwordHistory?: number;
|
|
||||||
/** The number of days before the password expires */
|
|
||||||
forceExpiredPasswordChange?: number;
|
|
||||||
};
|
};
|
||||||
|
@ -58,7 +58,6 @@ export type KcContextLike = {
|
|||||||
|
|
||||||
export type ParamsOfUseUserProfileForm = {
|
export type ParamsOfUseUserProfileForm = {
|
||||||
kcContext: KcContextLike;
|
kcContext: KcContextLike;
|
||||||
passwordValidators?: Validators;
|
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
passwordConfirmationDisabled?: boolean;
|
passwordConfirmationDisabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -74,7 +73,7 @@ export type ReturnTypeOfUseUserProfileForm = {
|
|||||||
* artificial password related attributes only if kcContext.passwordRequired === true
|
* artificial password related attributes only if kcContext.passwordRequired === true
|
||||||
*/
|
*/
|
||||||
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
|
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
|
||||||
const { kcContext, passwordValidators = {}, i18n, passwordConfirmationDisabled = false } = params;
|
const { kcContext, i18n, passwordConfirmationDisabled = false } = params;
|
||||||
|
|
||||||
const attributesWithPassword = useMemo(() => {
|
const attributesWithPassword = useMemo(() => {
|
||||||
const attributesWithPassword: Attribute[] = [];
|
const attributesWithPassword: Attribute[] = [];
|
||||||
@ -95,7 +94,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
"displayName": id<`\${${MessageKey}}`>("${password}"),
|
"displayName": id<`\${${MessageKey}}`>("${password}"),
|
||||||
"required": true,
|
"required": true,
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"validators": passwordValidators,
|
"validators": {},
|
||||||
"annotations": {},
|
"annotations": {},
|
||||||
"autocomplete": "new-password",
|
"autocomplete": "new-password",
|
||||||
"html5DataAnnotations": {},
|
"html5DataAnnotations": {},
|
||||||
@ -282,7 +281,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
|
|
||||||
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
||||||
function useGetErrors(params: {
|
function useGetErrors(params: {
|
||||||
kcContext: Pick<KcContextLike, "messagesPerField">;
|
kcContext: Pick<KcContextLike, "messagesPerField" | "passwordPolicies">;
|
||||||
attributes: {
|
attributes: {
|
||||||
name: string;
|
name: string;
|
||||||
validators: Validators;
|
validators: Validators;
|
||||||
@ -294,7 +293,7 @@ function useGetErrors(params: {
|
|||||||
}) {
|
}) {
|
||||||
const { kcContext, attributes, i18n } = params;
|
const { kcContext, attributes, i18n } = params;
|
||||||
|
|
||||||
const { messagesPerField } = kcContext;
|
const { messagesPerField, passwordPolicies } = kcContext;
|
||||||
|
|
||||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||||
|
|
||||||
@ -346,6 +345,206 @@ function useGetErrors(params: {
|
|||||||
|
|
||||||
const errors: FormFieldError[] = [];
|
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: {
|
password_confirm_matches_password: {
|
||||||
if (name !== "password-confirm") {
|
if (name !== "password-confirm") {
|
||||||
break password_confirm_matches_password;
|
break password_confirm_matches_password;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user