Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,552 changes: 0 additions & 1,552 deletions deno.lock

This file was deleted.

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/std@0.217.0/assert/mod.ts";
import { faker } from "https://deno.land/x/deno_faker@v1.0.3/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/std@0.217.0/assert/mod.ts";
import { faker } from "https://deno.land/x/deno_faker@v1.0.3/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