Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
feat: Create the user_passwords module
Browse files Browse the repository at this point in the history
  • Loading branch information
Blckbrry-Pi committed Aug 3, 2024
1 parent a119950 commit 91cb526
Show file tree
Hide file tree
Showing 15 changed files with 583 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "Passwords" (
"userId" UUID NOT NULL,
"passwordHash" TEXT NOT NULL,
"algo" TEXT NOT NULL,

CONSTRAINT "Passwords_pkey" PRIMARY KEY ("userId")
);
3 changes: 3 additions & 0 deletions modules/user_passwords/db/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
11 changes: 11 additions & 0 deletions modules/user_passwords/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Do not modify this `datasource` block
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Passwords {
userId String @db.Uuid @id
passwordHash String
algo String
}
45 changes: 45 additions & 0 deletions modules/user_passwords/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "User Password Verifier",
"description": "An INTERNAL-ONLY module to store and verify passwords by user ID. Used by some auth modules that require password verification.",
"icon": "shield-halved",
"tags": [
"core",
"user",
"auth",
"internal"
],
"authors": [
"rivet-gg",
"Blckbrry-Pi"
],
"status": "beta",
"dependencies": {
"users": {},
"rate_limit": {}
},
"scripts": {
"verify": {
"name": "Verify Password for User ID",
"description": "Verify that the provided password matches the provided user ID. Errors on mismatch."
},
"add": {
"name": "Add Password for User",
"description": "Register a new userID/password combination. Errors if user already has a password."
},
"update": {
"name": "Update Password for User",
"description": "Update a userID/password combination. Errors if user does not have a password."
}
},
"errors": {
"user_already_has_password": {
"name": "User already has a password"
},
"user_does_not_have_password": {
"name": "User does not yet have a password"
},
"password_invalid": {
"name": "Password is Invalid"
}
}
}
41 changes: 41 additions & 0 deletions modules/user_passwords/scripts/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Empty, RuntimeError, ScriptContext } from "../module.gen.ts";
import { ALGORITHM_DEFAULT, Algorithm, hash } from "../utils/common.ts";

export interface Request {
userId: string;
password: string;
algorithm?: Algorithm;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Check if the user exists before hashing the password to save compute
// resources
const user = await ctx.db.passwords.findFirst({
where: {
userId: req.userId,
},
});
if (user) {
throw new RuntimeError("user_already_has_password");
}

// Hash the password
const algo = req.algorithm || ALGORITHM_DEFAULT;
const passwordHash = await hash(req.password, algo);

// Create an entry for the user's password
await ctx.db.passwords.create({
data: {
userId: req.userId,
passwordHash,
algo,
},
});

return {};
}
43 changes: 43 additions & 0 deletions modules/user_passwords/scripts/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Empty, RuntimeError, ScriptContext } from "../module.gen.ts";
import { ALGORITHM_DEFAULT, Algorithm, hash } from "../utils/common.ts";

export interface Request {
userId: string;
newPassword: string;
newAlgorithm?: Algorithm;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Ensure the user exists before hashing the password to save compute
// resources
const user = await ctx.db.passwords.findFirst({
where: {
userId: req.userId,
},
});
if (!user) {
throw new RuntimeError("user_does_not_have_password");
}

// Hash the password
const algo = req.newAlgorithm || ALGORITHM_DEFAULT;
const passwordHash = await hash(req.newPassword, algo);

// Update the entry for the user's password
await ctx.db.passwords.update({
where: {
userId: req.userId,
},
data: {
passwordHash,
algo,
},
});

return {};
}
37 changes: 37 additions & 0 deletions modules/user_passwords/scripts/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Empty, RuntimeError, ScriptContext } from "../module.gen.ts";
import { Algorithm, hashMatches } from "../utils/common.ts";

export interface Request {
userId: string;
password: string;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
// Look up the user password hash
const user = await ctx.db.passwords.findFirst({
where: {
userId: req.userId,
},
select: {
algo: true,
passwordHash: true,
}
});
if (!user) throw new RuntimeError("user_does_not_have_password");

// Verify the passwordHash
const passwordMatches = await hashMatches(
req.password,
user.passwordHash,
user.algo as Algorithm,
);

if (!passwordMatches) throw new RuntimeError("password_invalid");

return {};
}
44 changes: 44 additions & 0 deletions modules/user_passwords/tests/algorithms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, TestContext } from "../module.gen.ts";
import { assertExists } from "https://deno.land/[email protected]/assert/mod.ts";
import { faker } from "https://deno.land/x/[email protected]/mod.ts";

test("algorithms", async (ctx: TestContext) => {
const { user } = await ctx.modules.users.create({
username: faker.internet.userName(),
});
assertExists(user);

// Set up user
await ctx.modules.userPasswords.add({ userId: user.id, password: "password" });

const algorithms = ["argon2", "bcrypt", "scrypt"] as const;
for (const algorithm of algorithms) {
// Register password
const password = faker.internet.password();
await ctx.modules.userPasswords.update({
userId: user.id,
newPassword: password,
newAlgorithm: algorithm,
});

// Verify password
await ctx.modules.userPasswords.verify({
userId: user.id,
password: password,
});

// Change password
const newPass = faker.internet.password();
await ctx.modules.userPasswords.update({
userId: user.id,
newPassword: newPass,
newAlgorithm: algorithm,
});

// Verify new password
await ctx.modules.userPasswords.verify({
userId: user.id,
password: newPass,
});
}
});
94 changes: 94 additions & 0 deletions modules/user_passwords/tests/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { test, TestContext } from "../module.gen.ts";
import { assertExists, assertEquals, assertRejects } from "https://deno.land/[email protected]/assert/mod.ts";
import { faker } from "https://deno.land/x/[email protected]/mod.ts";
import { RuntimeError } from "../module.gen.ts";

test("accept_matching_password", async (ctx: TestContext) => {
const { user } = await ctx.modules.users.create({
username: faker.internet.userName(),
});
assertExists(user);

// Register password
const password = faker.internet.password();
await ctx.modules.userPasswords.add({
userId: user.id,
password,
});

// Verify password
await ctx.modules.userPasswords.verify({
userId: user.id,
password: password,
});

// Change password
const newPass = faker.internet.password();
await ctx.modules.userPasswords.update({
userId: user.id,
newPassword: newPass,
});

// Verify new password
await ctx.modules.userPasswords.verify({
userId: user.id,
password: newPass,
});
});


test("reject_different_password", async (ctx: TestContext) => {
const { user } = await ctx.modules.users.create({
username: faker.internet.userName(),
});
assertExists(user);

// Register password
const password = faker.internet.password();
await ctx.modules.userPasswords.add({
userId: user.id,
password,
});

const wrongPassword = faker.internet.password();

// Verify incorrect password
const error = await assertRejects(async () => {
await ctx.modules.userPasswords.verify({
userId: user.id,
password: wrongPassword,
});
}, RuntimeError);

// Verify error message
assertExists(error.message);
assertEquals(error.code, "password_invalid");
});

test("reject_unregistered", async (ctx: TestContext) => {
const { user } = await ctx.modules.users.create({
username: faker.internet.userName(),
});
assertExists(user);

// Register password
const password = faker.internet.password();
await ctx.modules.userPasswords.add({
userId: user.id,
password,
});

const wrongPassword = faker.internet.password();

// Verify "correct" password with unregistered user
const error = await assertRejects(async () => {
await ctx.modules.userPasswords.verify({
userId: crypto.randomUUID(),
password: wrongPassword,
});
}, RuntimeError);

// Verify error message
assertExists(error.message);
assertEquals(error.code, "user_does_not_have_password");
});
Loading

0 comments on commit 91cb526

Please sign in to comment.