This repository was 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
feat: Create the user_passwords module
#127
Merged
NathanFlurry
merged 1 commit into
main
from
07-04-feat_create_the_user_passwords_module
Aug 24, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
modules/user_passwords/db/migrations/20240705023048_init/migration.sql
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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/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, | ||
| }); | ||
| } | ||
| }); |
This file contains hidden or 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/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"); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.