From bd8561a5c20232ab909de3e3f209b874139a452f Mon Sep 17 00:00:00 2001 From: Adrien Zaganelli Date: Wed, 25 Sep 2024 13:53:43 +0200 Subject: [PATCH 1/3] feat(passwrd): allow custom hasing methods for password #31 --- README.md | 4 ++++ src/runtime/core/core.ts | 21 +++++++++++++++----- src/runtime/core/email-and-password-utils.ts | 19 ++++++++++++++++++ src/runtime/core/password.ts | 21 -------------------- src/runtime/core/types.ts | 5 +++++ 5 files changed, 44 insertions(+), 26 deletions(-) delete mode 100644 src/runtime/core/password.ts diff --git a/README.md b/README.md index d05d78b..2d32832 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,10 @@ Sets a custom method for generating random session IDs. Sets a custom method for generating random email verification codes. +##### `setPasswordHashingMethods(fn: () => string)` + +Sets a custom method for generating random email verification codes. + ##### `getSession(sessionId: string)` Fetches a session by its session ID. diff --git a/src/runtime/core/core.ts b/src/runtime/core/core.ts index 932eee3..5e9a69e 100644 --- a/src/runtime/core/core.ts +++ b/src/runtime/core/core.ts @@ -1,15 +1,14 @@ import { createChecker, type supportedConnectors } from "drizzle-schema-checker"; import { getOAuthAccountsTableSchema, getSessionsTableSchema, getUsersTableSchema, getEmailVerificationCodesTableSchema } from "../database/lib/sqlite/schema.sqlite"; import { drizzle as drizzleIntegration } from "db0/integrations/drizzle/index"; -import type { ICreateOrLoginParams, ICreateUserParams, ILoginUserParams, ISlipAuthCoreOptions, SchemasMockValue, SlipAuthUser, tableNames } from "./types"; +import type { ICreateOrLoginParams, ICreateUserParams, ILoginUserParams, IPasswordHashingMethods, ISlipAuthCoreOptions, SchemasMockValue, SlipAuthUser, tableNames } from "./types"; import { createSlipHooks } from "./hooks"; import { UsersRepository } from "./repositories/UsersRepository"; import { SessionsRepository } from "./repositories/SessionsRepository"; import { OAuthAccountsRepository } from "./repositories/OAuthAccountsRepository"; import { EmailVerificationCodesRepository } from "./repositories/EmailVerificationCodesRepository"; import type { SlipAuthPublicSession } from "../types"; -import { defaultIdGenerationMethod, isValidEmail, defaultEmailVerificationCodeGenerationMethod } from "./email-and-password-utils"; -import { hashPassword, verifyPassword } from "./password"; +import { defaultIdGenerationMethod, isValidEmail, defaultEmailVerificationCodeGenerationMethod, defaultHashPasswordMethod, defaultVerifyPasswordMethod } from "./email-and-password-utils"; import { InvalidEmailOrPasswordError, UnhandledError } from "./errors/SlipAuthError.js"; import type { Database } from "db0"; import { isWithinExpirationDate } from "oslo"; @@ -30,6 +29,8 @@ export class SlipAuthCore { #createRandomSessionId: () => string; #createRandomEmailVerificationCode: () => string; + #passwordHashingMethods: IPasswordHashingMethods; + readonly schemas: SchemasMockValue; readonly hooks = createSlipHooks(); @@ -62,6 +63,11 @@ export class SlipAuthCore { this.#createRandomUserId = defaultIdGenerationMethod; this.#createRandomEmailVerificationCode = defaultEmailVerificationCodeGenerationMethod; + + this.#passwordHashingMethods = { + hash: defaultHashPasswordMethod, + verify: defaultHashPasswordMethod, + }; } public checkDbAndTables(dialect: supportedConnectors) { @@ -105,7 +111,7 @@ export class SlipAuthCore { throw new InvalidEmailOrPasswordError("no password oauth user"); } - const validPassword = await verifyPassword(existingUser.password, password); + const validPassword = await this.#passwordHashingMethods.verify(existingUser.password, password); if (!validPassword) { throw new InvalidEmailOrPasswordError("login invalid password"); } @@ -131,7 +137,7 @@ export class SlipAuthCore { } const userId = this.#createRandomUserId(); - const passwordHash = await hashPassword(password); + const passwordHash = await this.#passwordHashingMethods.hash(password); try { const user = await this.#repos.users.insert(userId, email, passwordHash); @@ -254,6 +260,11 @@ export class SlipAuthCore { this.#createRandomEmailVerificationCode = fn; } + public setPasswordHashingMethods(fn: () => IPasswordHashingMethods) { + const methods = fn(); + this.#passwordHashingMethods = methods; + } + public getSession(sessionId: string) { return this.#repos.sessions.findById(sessionId); } diff --git a/src/runtime/core/email-and-password-utils.ts b/src/runtime/core/email-and-password-utils.ts index eeabb5b..d0a3d2a 100644 --- a/src/runtime/core/email-and-password-utils.ts +++ b/src/runtime/core/email-and-password-utils.ts @@ -1,4 +1,6 @@ import { generateRandomString, alphabet } from "oslo/crypto"; +import type { Options as ArgonOptions } from "@node-rs/argon2"; +import { hash, verify } from "@node-rs/argon2"; /** https://thecopenhagenbook.com/email-verification#input-validation @@ -20,3 +22,20 @@ export function isValidEmail(email: string): boolean { export const defaultIdGenerationMethod = () => generateRandomString(15, alphabet("a-z", "A-Z", "0-9")); export const defaultEmailVerificationCodeGenerationMethod = () => generateRandomString(6, alphabet("0-9", "A-Z")); + +const hashOptions: ArgonOptions = { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, +}; + +// https://thecopenhagenbook.com/password-authentication#argon2id +export async function defaultHashPasswordMethod(rawPassword: string): Promise { + return await hash(rawPassword, hashOptions); +}; + +export async function defaultVerifyPasswordMethod(sourceHashedPassword: string, rawPassword: string): Promise { + return verify(sourceHashedPassword, rawPassword, hashOptions); +}; diff --git a/src/runtime/core/password.ts b/src/runtime/core/password.ts deleted file mode 100644 index bc6c668..0000000 --- a/src/runtime/core/password.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Options as ArgonOptions } from "@node-rs/argon2"; -import { hash, verify } from "@node-rs/argon2"; - -const hashOptions: ArgonOptions = { - // recommended minimum parameters - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1, -}; - -// TODO: implement salting -// https://thecopenhagenbook.com/password-authentication#argon2id -export function hashPassword(rawPassword: string) { - return hash(rawPassword, hashOptions); -} - -// TODO: implement salting -export function verifyPassword(sourceHashedPassword: string, rawPassword: string) { - return verify(sourceHashedPassword, rawPassword, hashOptions); -} diff --git a/src/runtime/core/types.ts b/src/runtime/core/types.ts index 3e3e995..6d44b85 100644 --- a/src/runtime/core/types.ts +++ b/src/runtime/core/types.ts @@ -47,6 +47,11 @@ export type SlipAuthOAuthAccount = ReturnType["$inferInsert"]; export type SlipAuthEmailVerificationCode = ReturnType["$inferSelect"]; +export interface IPasswordHashingMethods { + hash: (rawPassword: string) => Promise + verify: (sourceHashedPassword: string, rawPassword: string) => Promise +} + export interface ISlipAuthCoreOptions { /** * {@link https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L24} From cdacb8e366ae513b21a50fe89eb569d5cb8f78d1 Mon Sep 17 00:00:00 2001 From: Adrien Zaganelli Date: Wed, 25 Sep 2024 14:02:45 +0200 Subject: [PATCH 2/3] add tests #31 --- src/runtime/core/types.ts | 2 +- tests/core-email-password.spec.ts | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/runtime/core/types.ts b/src/runtime/core/types.ts index 6d44b85..a411e84 100644 --- a/src/runtime/core/types.ts +++ b/src/runtime/core/types.ts @@ -49,7 +49,7 @@ export type SlipAuthEmailVerificationCode = ReturnType Promise - verify: (sourceHashedPassword: string, rawPassword: string) => Promise + verify: (sourceHashedPassword: string, rawPassword: string) => Promise } export interface ISlipAuthCoreOptions { diff --git a/tests/core-email-password.spec.ts b/tests/core-email-password.spec.ts index 4f334e3..175a5e2 100644 --- a/tests/core-email-password.spec.ts +++ b/tests/core-email-password.spec.ts @@ -24,13 +24,14 @@ beforeEach(async () => { const defaultInsert = { email: "email@test.com", - password: "password", + password: "pa$$word", }; const mocks = vi.hoisted(() => { return { userCreatedCount: 0, sessionCreatedCount: 0, + passwordCount: 0, }; }); @@ -71,6 +72,21 @@ describe("SlipAuthCore", () => { mocks.sessionCreatedCount++; return `session-id-${mocks.sessionCreatedCount}`; }); + + function sanitizePassword(str: string) { + return str.replaceAll("$", "") + "$"; + } + auth.setPasswordHashingMethods(() => ({ + hash: async (password: string) => sanitizePassword(password) + mocks.passwordCount, + verify: async (sourceHashedPassword, rawPassword) => { + const salt = sourceHashedPassword.split("$").at(-1); + if (!salt) { + return false; + } + return sourceHashedPassword === sanitizePassword(rawPassword) + salt; + }, + }), + ); }); describe("register", () => { From 5ad187c53aeedab06a4c9d6c112d8f34d1dc7835 Mon Sep 17 00:00:00 2001 From: Adrien Zaganelli Date: Wed, 25 Sep 2024 14:04:28 +0200 Subject: [PATCH 3/3] fix typecheck --- src/runtime/core/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/core/core.ts b/src/runtime/core/core.ts index 5e9a69e..0b2b345 100644 --- a/src/runtime/core/core.ts +++ b/src/runtime/core/core.ts @@ -66,7 +66,7 @@ export class SlipAuthCore { this.#passwordHashingMethods = { hash: defaultHashPasswordMethod, - verify: defaultHashPasswordMethod, + verify: defaultVerifyPasswordMethod, }; }