This repository has been archived by the owner on Sep 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create the
user_passwords
module
- Loading branch information
1 parent
ba39943
commit 2674ef5
Showing
14 changed files
with
526 additions
and
1,552 deletions.
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
modules/user_passwords/db/migrations/20240705023048_init/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); |
Oops, something went wrong.