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
4 changes: 4 additions & 0 deletions modules/auth_email/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Config {
fromEmail?: string;
fromName?: string;
}
20 changes: 20 additions & 0 deletions modules/auth_email/db/migrations/20240807214202_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Verifications" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
"code" TEXT NOT NULL,
"token" TEXT NOT NULL,
"attemptCount" INTEGER NOT NULL DEFAULT 0,
"maxAttemptCount" INTEGER NOT NULL,
"createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expireAt" TIMESTAMP NOT NULL,
"completedAt" TIMESTAMP,

CONSTRAINT "Verifications_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Verifications_code_key" ON "Verifications"("code");

-- CreateIndex
CREATE UNIQUE INDEX "Verifications_token_key" ON "Verifications"("token");
3 changes: 3 additions & 0 deletions modules/auth_email/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"
21 changes: 21 additions & 0 deletions modules/auth_email/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Verifications {
id String @id @default(uuid()) @db.Uuid

email String

// Code the user has to input to verify the email
code String @unique
token String @unique

attemptCount Int @default(0)
maxAttemptCount Int

createdAt DateTime @default(now()) @db.Timestamp
expireAt DateTime @db.Timestamp
completedAt DateTime? @db.Timestamp
}
76 changes: 76 additions & 0 deletions modules/auth_email/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "Auth Email",
"description": "[INTERNAL-ONLY: use auth_email_password/auth_email_passwordless/auth_email_link.] Authenticating users with email only or an email/password combination.",
"icon": "key",
"tags": [
"core",
"auth",
"user"
],
"authors": [
"rivet-gg",
"NathanFlurry",
"Blckbrry-Pi"
],
"status": "stable",
"dependencies": {
"email": {},
"identities": {},
"users": {},
"tokens": {},
"user_passwords": {},
"rate_limit": {}
},
"defaultConfig": {
"fromEmail": "hello@test.com",
"fromName": "Authentication Code"
},
"scripts": {
"send_verification": {
"name": "Send Email Verification (No Password)",
"description": "Send a one-time verification code to an email address to verify ownership. Does not require a password."
},
"verify_add_no_pass": {
"name": "Verify and Add Email to Existing User (No Password)",
"description": "Verify a user's email address and register it with an existing account. Does not require a password."
},
"verify_login_or_create_no_pass": {
"name": "Verify and Login as (or Create) User (No Password)",
"description": "Verify the email address code and return a userToken to AN account (creates a new account if one doesn't exist). Does not require a password."
},
"verify_link_email": {
"name": "Verify and Link Email Address to User",
"description": "Verify a user's email address and link it to an existing account without allowing login passwordless."
},

"verify_sign_up_email_pass": {
"name": "Verify and Sign Up with Email and Password",
"description": "Sign up a new user with an email and password."
},
"sign_in_email_pass": {
"name": "Sign In with Email and Password",
"description": "Sign in a user with an email and password."
},
"verify_add_email_pass": {
"name": "Verify and Add Email and Password to existing user",
"description": "Verify a user's email address and register it with an existing account. Requires a password."
}
},
"errors": {
"verification_code_invalid": {
"name": "Verification Code Invalid"
},
"verification_code_attempt_limit": {
"name": "Verification Code Attempt Limit"
},
"verification_code_expired": {
"name": "Verification Code Expired"
},
"verification_code_already_used": {
"name": "Verification Code Already Used"
},
"email_already_used": {
"name": "Email Already Used"
}
}
}
38 changes: 38 additions & 0 deletions modules/auth_email/scripts/send_verification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ScriptContext } from "../module.gen.ts";
import { createVerification } from "../utils/code_management.ts";
import { Verification } from "../utils/types.ts";

export interface Request {
email: string;
userToken?: string;
}

export interface Response {
verification: Verification;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

const { code, verification } = await createVerification(
ctx,
req.email,
);

// Send email
await ctx.modules.email.sendEmail({
from: {
email: ctx.config.fromEmail ?? "hello@test.com",
name: ctx.config.fromName ?? "Authentication Code",
},
to: [{ email: req.email }],
subject: "Your verification code",
text: `Your verification code is: ${code}`,
html: `Your verification code is: <b>${code}</b>`,
});

return { verification };
}
35 changes: 35 additions & 0 deletions modules/auth_email/scripts/sign_in_email_pass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ScriptContext } from "../module.gen.ts";
import { IDENTITY_INFO_PASSWORD } from "../utils/provider.ts";

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

export interface Response {
userToken: string;
userId: string;
}

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

// Try signing in with the email
const { userToken, userId } = await ctx.modules.identities.signIn({
info: IDENTITY_INFO_PASSWORD,
uniqueData: {
identifier: req.email,
},
});

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

return { userToken, userId };
}
77 changes: 77 additions & 0 deletions modules/auth_email/scripts/verify_add_email_pass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Empty, RuntimeError, ScriptContext } from "../module.gen.ts";
import { verifyCode } from "../utils/code_management.ts";
import { IDENTITY_INFO_PASSWORD } from "../utils/provider.ts";
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";

export interface Request {
userToken: string;

email: string;
password: string;
oldPassword: string | null;

verificationToken: string;
code: string;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

// Check the verification code. If it is valid, but for the wrong email, say
// the verification failed.
const { email } = await verifyCode(ctx, req.verificationToken, req.code);
if (!compareConstantTime(req.email, email)) {
throw new RuntimeError("verification_failed");
}

// Ensure that the email is not associated with ANY accounts in ANY way.
const providedUser = await ctx.modules.users.authenticateToken({
userToken: req.userToken,
});
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));

// If an old password was provided, ensure it was correct and update it.
// If one was not, register the user with the `userPasswords` module.
if (req.oldPassword) {
await ctx.modules.userPasswords.verify({
userId: providedUser.userId,
password: req.oldPassword,
});
await ctx.modules.userPasswords.update({
userId: providedUser.userId,
newPassword: req.password,
});
} else {
await ctx.modules.userPasswords.add({
userId: providedUser.userId,
password: req.password,
});
}

// Sign up the user with the passwordless email identity
await ctx.modules.identities.link({
userToken: req.userToken,
info: IDENTITY_INFO_PASSWORD,
uniqueData: {
identifier: email,
},
additionalData: {},
});

return {};
}

function compareConstantTime(aConstant: string, b: string) {
let isEq = 1;
for (let i = 0; i < aConstant.length; i++) {
isEq &= Number(aConstant[i] === b[i]);
}
isEq &= Number(aConstant.length === b.length);

return Boolean(isEq);
}
40 changes: 40 additions & 0 deletions modules/auth_email/scripts/verify_add_no_pass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Empty, ScriptContext } from "../module.gen.ts";
import { verifyCode } from "../utils/code_management.ts";
import { IDENTITY_INFO_PASSWORDLESS } from "../utils/provider.ts";
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";

export interface Request {
verificationToken: string;
code: string;
userToken: string;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

// Verify that the code is correct and valid
const { email } = await verifyCode(ctx, req.verificationToken, req.code);

// Ensure that the email is not already associated with another account
const providedUser = await ctx.modules.users.authenticateToken({
userToken: req.userToken,
});
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));

// Add email passwordless sign in to the user's account
await ctx.modules.identities.link({
userToken: req.userToken,
info: IDENTITY_INFO_PASSWORDLESS,
uniqueData: {
identifier: email,
},
additionalData: {},
});

return {};
}
40 changes: 40 additions & 0 deletions modules/auth_email/scripts/verify_link_email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Empty, ScriptContext } from "../module.gen.ts";
import { verifyCode } from "../utils/code_management.ts";
import { IDENTITY_INFO_LINK } from "../utils/provider.ts";
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";

export interface Request {
verificationToken: string;
code: string;
userToken: string;
}

export type Response = Empty;

export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
await ctx.modules.rateLimit.throttlePublic({});

// Verify that the code is correct and valid
const { email } = await verifyCode(ctx, req.verificationToken, req.code);

// Ensure that the email is not already associated with another account
const providedUser = await ctx.modules.users.authenticateToken({
userToken: req.userToken,
});
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));

// Link the email to the user's account
await ctx.modules.identities.link({
userToken: req.userToken,
info: IDENTITY_INFO_LINK,
uniqueData: {
identifier: email,
},
additionalData: {},
});

return {};
}
Loading