Compare commits
1 Commits
9ef7114c77
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a7ec0338 |
@@ -5,7 +5,6 @@ import {
|
|||||||
PASSWORD_MAX_LENGTH,
|
PASSWORD_MAX_LENGTH,
|
||||||
PASSWORD_MIN_LENGTH,
|
PASSWORD_MIN_LENGTH,
|
||||||
PASSWORD_POLICY_MESSAGE,
|
PASSWORD_POLICY_MESSAGE,
|
||||||
checkPasswordPolicy,
|
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
|
|
||||||
export type SetupResult =
|
export type SetupResult =
|
||||||
@@ -27,13 +26,6 @@ export async function createFirstAdmin(formData: {
|
|||||||
) {
|
) {
|
||||||
return { error: "validation", message: PASSWORD_POLICY_MESSAGE };
|
return { error: "validation", message: PASSWORD_POLICY_MESSAGE };
|
||||||
}
|
}
|
||||||
const policy = checkPasswordPolicy(formData.password, {
|
|
||||||
email: formData.email,
|
|
||||||
name: formData.name,
|
|
||||||
});
|
|
||||||
if (!policy.ok) {
|
|
||||||
return { error: "validation", message: policy.reason };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOCTOU guard — check again inside the action
|
// TOCTOU guard — check again inside the action
|
||||||
const count = await prisma.user.count();
|
const count = await prisma.user.count();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
PASSWORD_MAX_LENGTH,
|
PASSWORD_MAX_LENGTH,
|
||||||
PASSWORD_MIN_LENGTH,
|
PASSWORD_MIN_LENGTH,
|
||||||
PASSWORD_POLICY_MESSAGE,
|
PASSWORD_POLICY_MESSAGE,
|
||||||
checkPasswordPolicy,
|
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -134,17 +133,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject weak/common/identity-related passwords *after* the token is
|
|
||||||
// validated so attackers can't probe the policy without a valid link.
|
|
||||||
const userForPolicy = await ctx.db.user.findUnique({
|
|
||||||
where: { email: record.email },
|
|
||||||
select: { email: true, name: true },
|
|
||||||
});
|
|
||||||
const policy = checkPasswordPolicy(input.password, userForPolicy ?? undefined);
|
|
||||||
if (!policy.ok) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: policy.reason });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hash } = await import("@node-rs/argon2");
|
const { hash } = await import("@node-rs/argon2");
|
||||||
const passwordHash = await hash(input.password);
|
const passwordHash = await hash(input.password);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
PASSWORD_MAX_LENGTH,
|
PASSWORD_MAX_LENGTH,
|
||||||
PASSWORD_MIN_LENGTH,
|
PASSWORD_MIN_LENGTH,
|
||||||
PASSWORD_POLICY_MESSAGE,
|
PASSWORD_POLICY_MESSAGE,
|
||||||
checkPasswordPolicy,
|
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
|
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
|
||||||
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
||||||
@@ -156,11 +155,6 @@ export const inviteRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const policy = checkPasswordPolicy(input.password, { email: invite.email });
|
|
||||||
if (!policy.ok) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: policy.reason });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hash } = await import("@node-rs/argon2");
|
const { hash } = await import("@node-rs/argon2");
|
||||||
const passwordHash = await hash(input.password);
|
const passwordHash = await hash(input.password);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
PASSWORD_MAX_LENGTH,
|
PASSWORD_MAX_LENGTH,
|
||||||
PASSWORD_MIN_LENGTH,
|
PASSWORD_MIN_LENGTH,
|
||||||
PASSWORD_POLICY_MESSAGE,
|
PASSWORD_POLICY_MESSAGE,
|
||||||
checkPasswordPolicy,
|
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
import { PermissionOverrides, SystemRole, resolvePermissions } from "@capakraken/shared/types";
|
import { PermissionOverrides, SystemRole, resolvePermissions } from "@capakraken/shared/types";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -122,11 +121,6 @@ export async function createUser(
|
|||||||
throw new TRPCError({ code: "CONFLICT", message: "User with this email already exists" });
|
throw new TRPCError({ code: "CONFLICT", message: "User with this email already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const policy = checkPasswordPolicy(input.password, { email: input.email, name: input.name });
|
|
||||||
if (!policy.ok) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: policy.reason });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hash } = await import("@node-rs/argon2");
|
const { hash } = await import("@node-rs/argon2");
|
||||||
const passwordHash = await hash(input.password);
|
const passwordHash = await hash(input.password);
|
||||||
|
|
||||||
@@ -175,11 +169,6 @@ export async function setUserPassword(
|
|||||||
"User",
|
"User",
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy = checkPasswordPolicy(input.password, { email: user.email, name: user.name });
|
|
||||||
if (!policy.ok) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: policy.reason });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hash } = await import("@node-rs/argon2");
|
const { hash } = await import("@node-rs/argon2");
|
||||||
const passwordHash = await hash(input.password);
|
const passwordHash = await hash(input.password);
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ describe("readWorksheetMatrix", () => {
|
|||||||
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
||||||
`exceeds the ${MAX_DISPO_WORKBOOK_ROWS} row import limit`,
|
`exceeds the ${MAX_DISPO_WORKBOOK_ROWS} row import limit`,
|
||||||
);
|
);
|
||||||
}, 30000);
|
}, 60000);
|
||||||
|
|
||||||
it("rejects worksheets that exceed the column limit", async () => {
|
it("rejects worksheets that exceed the column limit", async () => {
|
||||||
const directory = await makeTempDirectory();
|
const directory = await makeTempDirectory();
|
||||||
@@ -149,7 +149,7 @@ describe("readWorksheetMatrix", () => {
|
|||||||
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
await expect(readWorksheetMatrix(workbookPath, "Sheet1")).rejects.toThrow(
|
||||||
`exceeds the ${MAX_DISPO_WORKBOOK_COLUMNS} column import limit`,
|
`exceeds the ${MAX_DISPO_WORKBOOK_COLUMNS} column import limit`,
|
||||||
);
|
);
|
||||||
}, 30000);
|
}, 60000);
|
||||||
|
|
||||||
describe("DISPO_IMPORT_DIR allowlist", () => {
|
describe("DISPO_IMPORT_DIR allowlist", () => {
|
||||||
it("rejects absolute paths that escape the configured import dir", async () => {
|
it("rejects absolute paths that escape the configured import dir", async () => {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { checkPasswordPolicy } from "../security/password-policy.js";
|
|
||||||
|
|
||||||
describe("checkPasswordPolicy", () => {
|
|
||||||
describe("length bounds", () => {
|
|
||||||
it("rejects passwords shorter than 12 chars", () => {
|
|
||||||
const result = checkPasswordPolicy("short1!");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/at least 12/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects passwords longer than 128 chars", () => {
|
|
||||||
const result = checkPasswordPolicy("A".repeat(129));
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/no more than 128/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts passwords at the lower bound that pass other checks", () => {
|
|
||||||
const result = checkPasswordPolicy("Tr0ub4dor&3!"); // 12 chars, varied
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trivial patterns", () => {
|
|
||||||
it("rejects single char repeated", () => {
|
|
||||||
const result = checkPasswordPolicy("aaaaaaaaaaaa");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/single character/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects short patterns repeated", () => {
|
|
||||||
const result = checkPasswordPolicy("abcabcabcabc");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/short pattern/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects '1212121212121212' (2-char pattern repeated)", () => {
|
|
||||||
const result = checkPasswordPolicy("1212121212121212");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/short pattern/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects keyboard sequences like 'abcdefghijkl'", () => {
|
|
||||||
const result = checkPasswordPolicy("abcdefghijkl");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/sequence/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects numeric runs like '1234567890ab'", () => {
|
|
||||||
const result = checkPasswordPolicy("1234567890ab");
|
|
||||||
// Either matches blacklist or sequence detector — both rejections OK.
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("common-password blacklist", () => {
|
|
||||||
it("rejects 'PasswordPassword' (case-insensitive)", () => {
|
|
||||||
const result = checkPasswordPolicy("PasswordPassword");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/commonly used/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects 'Welcome2026!' seasonal password", () => {
|
|
||||||
const result = checkPasswordPolicy("Welcome2026!");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/commonly used/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects 'Summer2025!' regardless of case", () => {
|
|
||||||
const result = checkPasswordPolicy("SUMMER2025!");
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("identity inclusion", () => {
|
|
||||||
it("rejects passwords containing the email local-part", () => {
|
|
||||||
const result = checkPasswordPolicy("hartmutSomePass1", {
|
|
||||||
email: "hartmut@example.com",
|
|
||||||
});
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/email or name/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects passwords containing the user name", () => {
|
|
||||||
const result = checkPasswordPolicy("MyHartmutPass1!", {
|
|
||||||
name: "Hartmut Noerenberg",
|
|
||||||
});
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
expect(result.reason).toMatch(/email or name/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores short email locals to avoid false positives", () => {
|
|
||||||
const result = checkPasswordPolicy("XyZmagic12345!", { email: "x@example.com" });
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores short names (<4 chars)", () => {
|
|
||||||
const result = checkPasswordPolicy("XyZmagic12345!", { name: "Ed" });
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("strong passwords", () => {
|
|
||||||
it("accepts a 16-char random-looking passphrase", () => {
|
|
||||||
const result = checkPasswordPolicy("Tr0ub4d0r&3-x9Q!");
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts diceware-style passphrase", () => {
|
|
||||||
const result = checkPasswordPolicy("correct-horse-battery-staple-7!");
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./schemas/index.js";
|
export * from "./schemas/index.js";
|
||||||
export * from "./constants/index.js";
|
export * from "./constants/index.js";
|
||||||
export * from "./security/index.js";
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./password-policy.js";
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import {
|
|
||||||
PASSWORD_MAX_LENGTH,
|
|
||||||
PASSWORD_MIN_LENGTH,
|
|
||||||
PASSWORD_POLICY_MESSAGE,
|
|
||||||
} from "../constants/index.js";
|
|
||||||
|
|
||||||
export interface PasswordContext {
|
|
||||||
email?: string | null;
|
|
||||||
name?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PasswordCheckResult = { ok: true } | { ok: false; reason: string };
|
|
||||||
|
|
||||||
// Curated list of >=12-char passwords that pass the length gate but are
|
|
||||||
// known weak (rockyou top entries, predictable seasonal patterns, common
|
|
||||||
// admin defaults). Stored lower-cased for case-insensitive matching. The
|
|
||||||
// vast majority of weak passwords are <12 chars and already rejected by
|
|
||||||
// length, so this list focuses on what would otherwise *pass* min-length.
|
|
||||||
const COMMON_PASSWORDS: ReadonlySet<string> = new Set([
|
|
||||||
"passwordpassword",
|
|
||||||
"password1234",
|
|
||||||
"password12345",
|
|
||||||
"password123456",
|
|
||||||
"password1234567",
|
|
||||||
"passwordpassword1",
|
|
||||||
"passw0rdpassw0rd",
|
|
||||||
"1234567890ab",
|
|
||||||
"1234567890abc",
|
|
||||||
"12345678901",
|
|
||||||
"123456789012",
|
|
||||||
"1234567891234",
|
|
||||||
"qwertyuiop12",
|
|
||||||
"qwertyuiop123",
|
|
||||||
"qwertyuiop1234",
|
|
||||||
"qwertyuiopas",
|
|
||||||
"asdfghjkl123",
|
|
||||||
"asdfghjkl1234",
|
|
||||||
"iloveyou1234",
|
|
||||||
"iloveyou12345",
|
|
||||||
"iloveyouabc1",
|
|
||||||
"iloveyouforever",
|
|
||||||
"ilovemybabies",
|
|
||||||
"letmein123456",
|
|
||||||
"letmein12345",
|
|
||||||
"welcome12345",
|
|
||||||
"welcome123456",
|
|
||||||
"welcome1234567",
|
|
||||||
"admin1234567",
|
|
||||||
"admin12345678",
|
|
||||||
"administrator1",
|
|
||||||
"administrator123",
|
|
||||||
"changeme1234",
|
|
||||||
"changeme12345",
|
|
||||||
"default1234567",
|
|
||||||
"football1234",
|
|
||||||
"baseball1234",
|
|
||||||
"trustno112345",
|
|
||||||
"summer2023!",
|
|
||||||
"summer2024!",
|
|
||||||
"summer2025!",
|
|
||||||
"summer2026!",
|
|
||||||
"winter2023!",
|
|
||||||
"winter2024!",
|
|
||||||
"winter2025!",
|
|
||||||
"winter2026!",
|
|
||||||
"spring2023!",
|
|
||||||
"spring2024!",
|
|
||||||
"spring2025!",
|
|
||||||
"spring2026!",
|
|
||||||
"autumn2023!",
|
|
||||||
"autumn2024!",
|
|
||||||
"autumn2025!",
|
|
||||||
"autumn2026!",
|
|
||||||
"welcome2023!",
|
|
||||||
"welcome2024!",
|
|
||||||
"welcome2025!",
|
|
||||||
"welcome2026!",
|
|
||||||
"password2023!",
|
|
||||||
"password2024!",
|
|
||||||
"password2025!",
|
|
||||||
"password2026!",
|
|
||||||
"p@ssw0rd1234",
|
|
||||||
"p@ssword1234",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function isSingleCharRepeated(pw: string): boolean {
|
|
||||||
if (pw.length === 0) return false;
|
|
||||||
const first = pw[0]!;
|
|
||||||
for (let i = 1; i < pw.length; i++) {
|
|
||||||
if (pw[i] !== first) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect monotonically +/-1 character runs. A password where >=60% of
|
|
||||||
// characters belong to a run of length >=5 (e.g. "abcdefgh1234") is
|
|
||||||
// rejected as a keyboard / numeric sequence. The 60% threshold lets
|
|
||||||
// passwords with a sequential prefix and a long random suffix pass.
|
|
||||||
function isSequentialPassword(pw: string): boolean {
|
|
||||||
if (pw.length < 8) return false;
|
|
||||||
const lower = pw.toLowerCase();
|
|
||||||
let runChars = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (i < lower.length) {
|
|
||||||
let runLen = 1;
|
|
||||||
while (i + runLen < lower.length) {
|
|
||||||
const delta = lower.charCodeAt(i + runLen) - lower.charCodeAt(i + runLen - 1);
|
|
||||||
if (delta === 1 || delta === -1) {
|
|
||||||
runLen++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (runLen >= 5) runChars += runLen;
|
|
||||||
i += runLen;
|
|
||||||
}
|
|
||||||
return runChars * 100 >= lower.length * 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject passwords that are a 2-6 char pattern repeated >=3 times
|
|
||||||
// (e.g. "abcabcabcabc", "12121212"). Length divisibility check first
|
|
||||||
// keeps the constant-factor work tiny.
|
|
||||||
function isShortPatternRepeated(pw: string): boolean {
|
|
||||||
for (let patLen = 2; patLen <= 6; patLen++) {
|
|
||||||
if (pw.length % patLen !== 0) continue;
|
|
||||||
const repeats = pw.length / patLen;
|
|
||||||
if (repeats < 3) continue;
|
|
||||||
const pattern = pw.slice(0, patLen);
|
|
||||||
let allMatch = true;
|
|
||||||
for (let r = 1; r < repeats; r++) {
|
|
||||||
if (pw.slice(r * patLen, (r + 1) * patLen) !== pattern) {
|
|
||||||
allMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allMatch) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substrings shorter than 4 chars are ignored: short names ("ed") or
|
|
||||||
// generic email locals ("ab") would otherwise reject too many legitimate
|
|
||||||
// passwords by coincidence.
|
|
||||||
function containsIdentity(pw: string, ctx: PasswordContext | undefined): boolean {
|
|
||||||
if (!ctx) return false;
|
|
||||||
const lower = pw.toLowerCase();
|
|
||||||
if (ctx.email) {
|
|
||||||
const local = ctx.email.split("@")[0]?.toLowerCase().trim() ?? "";
|
|
||||||
if (local.length >= 4 && lower.includes(local)) return true;
|
|
||||||
}
|
|
||||||
if (ctx.name) {
|
|
||||||
const lowered = ctx.name.toLowerCase().trim();
|
|
||||||
const fullNoSpaces = lowered.replace(/\s+/g, "");
|
|
||||||
if (fullNoSpaces.length >= 4 && lower.includes(fullNoSpaces)) return true;
|
|
||||||
// Also reject if the password embeds an individual name component
|
|
||||||
// (e.g. given-name or surname). "Hartmut Noerenberg" → check both
|
|
||||||
// "hartmut" and "noerenberg" so a password like "MyHartmutPass1!"
|
|
||||||
// is rejected, not only the unrealistic "MyHartmutNoerenbergPass1!".
|
|
||||||
for (const part of lowered.split(/\s+/)) {
|
|
||||||
if (part.length >= 4 && lower.includes(part)) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a password against the policy. Pure synchronous function so it
|
|
||||||
* can be called from server (tRPC mutations, server actions) and client
|
|
||||||
* (pre-submit validation) with identical results.
|
|
||||||
*/
|
|
||||||
export function checkPasswordPolicy(password: string, ctx?: PasswordContext): PasswordCheckResult {
|
|
||||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
|
||||||
return { ok: false, reason: PASSWORD_POLICY_MESSAGE };
|
|
||||||
}
|
|
||||||
if (password.length > PASSWORD_MAX_LENGTH) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: `Password must be no more than ${PASSWORD_MAX_LENGTH} characters.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (isSingleCharRepeated(password)) {
|
|
||||||
return { ok: false, reason: "Password must not be a single character repeated." };
|
|
||||||
}
|
|
||||||
if (isShortPatternRepeated(password)) {
|
|
||||||
return { ok: false, reason: "Password must not be a short pattern repeated." };
|
|
||||||
}
|
|
||||||
if (isSequentialPassword(password)) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "Password must not be a keyboard or numeric sequence.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (COMMON_PASSWORDS.has(password.toLowerCase())) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "Password is in the list of commonly used or breached passwords.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (containsIdentity(password, ctx)) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "Password must not contain your email or name.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user