diff --git a/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx index 24c422c7b..62edf64ea 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/PasswordInput/index.tsx @@ -194,6 +194,10 @@ function NewPasswordValidation({ }, { count: 1 } ) + case "allowedCharacters": + return intl.formatMessage({ + defaultMessage: "Only allowed characters", + }) } } diff --git a/packages/common/utils/zod/passwordValidator.test.ts b/packages/common/utils/zod/passwordValidator.test.ts new file mode 100644 index 000000000..1b5435013 --- /dev/null +++ b/packages/common/utils/zod/passwordValidator.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "vitest" + +import { passwordValidator } from "./passwordValidator" + +// Test cases to find differences between Curity and Zod validation +const testCases = [ + // Valid passwords + { password: "Password123!", description: "standard valid password" }, + { password: "ValidPass1@", description: "valid with @" }, + { password: "TestPass9#", description: "valid with #" }, + + // Edge cases for length + { password: "Pass123!", description: "8 characters (too short for both)" }, + { password: "ValidPass1!", description: "10 characters (minimum)" }, + { password: "A".repeat(40) + "1!", description: "40 characters (maximum)" }, + { password: "A".repeat(41) + "1!", description: "41 characters (too long)" }, + + // Special character differences + { password: "Password123*", description: "with asterisk" }, + { password: "Password123-", description: "with hyphen" }, + { password: "Password123=", description: "with equals" }, + { password: "Password123+", description: "with plus" }, + { password: "Password123_", description: "with underscore" }, + { password: "Password123&", description: "with ampersand" }, + { password: "Password123?", description: "with question mark" }, + { password: "Password123(", description: "with opening parenthesis" }, + { password: "Password123)", description: "with closing parenthesis" }, + + // International characters (Curity supports these, Zod might not) + { password: "Påssword123!", description: "with å" }, + { password: "Pässword123!", description: "with ä" }, + { password: "Pöössword123!", description: "with ö" }, + { password: "Pæssword123!", description: "with æ" }, + { password: "Pøssword123!", description: "with ø" }, + { password: "Püssword123!", description: "with ü" }, + { password: "Paßsword123!", description: "with ß" }, + + // Mixed case international + { password: "PÅSSWORD123!", description: "with uppercase Å" }, + { password: "PÄSSWORD123!", description: "with uppercase Ä" }, + { password: "PÖÖSSWORD123!", description: "with uppercase Ö" }, + + // Special characters not in Curity regex + { password: "Password123~", description: "with tilde (not in Curity)" }, + { password: "Password123`", description: "with backtick (not in Curity)" }, + { password: "Password123|", description: "with pipe (not in Curity)" }, + { password: "Password123\\", description: "with backslash (not in Curity)" }, + { + password: "Password123/", + description: "with forward slash (not in Curity)", + }, + { password: "Password123<", description: "with less than (not in Curity)" }, + { + password: "Password123>", + description: "with greater than (not in Curity)", + }, + { password: "Password123.", description: "with period (not in Curity)" }, + { password: "Password123,", description: "with comma (not in Curity)" }, + { password: "Password123;", description: "with semicolon (not in Curity)" }, + { password: "Password123:", description: "with colon (not in Curity)" }, + { password: "Password123'", description: "with apostrophe (not in Curity)" }, + { password: 'Password123"', description: "with quote (not in Curity)" }, + { + password: "Password123[", + description: "with opening bracket (not in Curity)", + }, + { + password: "Password123]", + description: "with closing bracket (not in Curity)", + }, + { + password: "Password123{", + description: "with opening brace (not in Curity)", + }, + { + password: "Password123}", + description: "with closing brace (not in Curity)", + }, +] + +const curityPasswordRegex = new RegExp( + "^(?!^.{41})(?=.{10,})(?=.*[0-9])(?=.*[a-zåäöæøüß])(?=.*[A-ZÅÄÖÆØÜ])(?=.*[&!?()@#$%^+=_\*\-])[A-Za-zåäöæøüßÅÄÖÆØÜ0-9&!?()@#$%^+=_\*\-]+$", + "g" +) + +describe("Should validate password the same way as Curity", () => { + beforeEach(() => { + // reset regex state before test + curityPasswordRegex.lastIndex = 0 + }) + test.each(testCases)("$description", ({ password }) => { + console.log(password) + const curityResult = curityPasswordRegex.test(password) + const zodResult = passwordValidator().safeParse(password) + + expect(zodResult.success).toBe(curityResult) + }) +}) diff --git a/packages/common/utils/zod/passwordValidator.ts b/packages/common/utils/zod/passwordValidator.ts index a3a1fcd3d..63440a3bf 100644 --- a/packages/common/utils/zod/passwordValidator.ts +++ b/packages/common/utils/zod/passwordValidator.ts @@ -7,11 +7,11 @@ export const passwordValidators = { message: "10 to 40 characters", }, hasUppercase: { - matcher: (password: string) => /[A-Z]/.test(password), + matcher: (password: string) => /[A-ZÅÄÖÆØÜ]/.test(password), message: "1 uppercase letter", }, hasLowercase: { - matcher: (password: string) => /[a-z]/.test(password), + matcher: (password: string) => /[a-zåäöæøüß]/.test(password), message: "1 lowercase letter", }, hasNumber: { @@ -19,16 +19,23 @@ export const passwordValidators = { message: "1 number", }, hasSpecialChar: { - matcher: (password: string) => - /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(password), + matcher: (password: string) => /[&!?()@#$%^+=_\*\-]+/.test(password), message: "1 special character", }, + allowedCharacters: { + matcher: (password: string) => + /^[A-Za-zåäöæøüßÅÄÖÆØÜ0-9&!?()@#$%^+=_\*\-]+$/.test(password), + message: "Only allowed characters", + }, } export const passwordValidator = (msg = "Required field") => z .string() .min(1, msg) + .refine(passwordValidators.allowedCharacters.matcher, { + message: passwordValidators.allowedCharacters.message, + }) .refine(passwordValidators.length.matcher, { message: passwordValidators.length.message, })