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

Commit 658d5a4

Browse files
committed
feat: Create an OAuth2 module for authenticating users
1 parent 544c215 commit 658d5a4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2427
-69
lines changed

modules/auth_email/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Config {
2+
fromEmail?: string;
3+
fromName?: string;
4+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- CreateTable
2+
CREATE TABLE "Verifications" (
3+
"id" UUID NOT NULL,
4+
"email" TEXT NOT NULL,
5+
"code" TEXT NOT NULL,
6+
"token" TEXT NOT NULL,
7+
"attemptCount" INTEGER NOT NULL DEFAULT 0,
8+
"maxAttemptCount" INTEGER NOT NULL,
9+
"createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
"expireAt" TIMESTAMP NOT NULL,
11+
"completedAt" TIMESTAMP,
12+
13+
CONSTRAINT "Verifications_pkey" PRIMARY KEY ("id")
14+
);
15+
16+
-- CreateIndex
17+
CREATE UNIQUE INDEX "Verifications_code_key" ON "Verifications"("code");
18+
19+
-- CreateIndex
20+
CREATE UNIQUE INDEX "Verifications_token_key" ON "Verifications"("token");
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Please do not edit this file manually
2+
# It should be added in your version-control system (i.e. Git)
3+
provider = "postgresql"

modules/auth_email/db/schema.prisma

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
datasource db {
2+
provider = "postgresql"
3+
url = env("DATABASE_URL")
4+
}
5+
6+
model Verifications {
7+
id String @id @default(uuid()) @db.Uuid
8+
9+
email String
10+
11+
// Code the user has to input to verify the email
12+
code String @unique
13+
token String @unique
14+
15+
attemptCount Int @default(0)
16+
maxAttemptCount Int
17+
18+
createdAt DateTime @default(now()) @db.Timestamp
19+
expireAt DateTime @db.Timestamp
20+
completedAt DateTime? @db.Timestamp
21+
}

modules/auth_email/module.json

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"name": "Auth Email",
3+
"description": "[INTERNAL-ONLY: use auth_email_password/auth_email_passwordless/auth_email_link.] Authenticating users with email only or an email/password combination.",
4+
"icon": "key",
5+
"tags": [
6+
"core",
7+
"auth",
8+
"user"
9+
],
10+
"authors": [
11+
"rivet-gg",
12+
"NathanFlurry",
13+
"Blckbrry-Pi"
14+
],
15+
"status": "stable",
16+
"dependencies": {
17+
"email": {},
18+
"identities": {},
19+
"users": {},
20+
"tokens": {},
21+
"user_passwords": {},
22+
"rate_limit": {}
23+
},
24+
"defaultConfig": {
25+
"fromEmail": "hello@test.com",
26+
"fromName": "Authentication Code"
27+
},
28+
"scripts": {
29+
"send_verification": {
30+
"name": "Send Email Verification (No Password)",
31+
"description": "Send a one-time verification code to an email address to verify ownership. Does not require a password."
32+
},
33+
"verify_add_no_pass": {
34+
"name": "Verify and Add Email to Existing User (No Password)",
35+
"description": "Verify a user's email address and register it with an existing account. Does not require a password."
36+
},
37+
"verify_login_or_create_no_pass": {
38+
"name": "Verify and Login as (or Create) User (No Password)",
39+
"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."
40+
},
41+
"verify_link_email": {
42+
"name": "Verify and Link Email Address to User",
43+
"description": "Verify a user's email address and link it to an existing account without allowing login passwordless."
44+
},
45+
46+
"verify_sign_up_email_pass": {
47+
"name": "Verify and Sign Up with Email and Password",
48+
"description": "Sign up a new user with an email and password."
49+
},
50+
"sign_in_email_pass": {
51+
"name": "Sign In with Email and Password",
52+
"description": "Sign in a user with an email and password."
53+
},
54+
"verify_add_email_pass": {
55+
"name": "Verify and Add Email and Password to existing user",
56+
"description": "Verify a user's email address and register it with an existing account. Requires a password."
57+
}
58+
},
59+
"errors": {
60+
"verification_code_invalid": {
61+
"name": "Verification Code Invalid"
62+
},
63+
"verification_code_attempt_limit": {
64+
"name": "Verification Code Attempt Limit"
65+
},
66+
"verification_code_expired": {
67+
"name": "Verification Code Expired"
68+
},
69+
"verification_code_already_used": {
70+
"name": "Verification Code Already Used"
71+
},
72+
"email_already_used": {
73+
"name": "Email Already Used"
74+
}
75+
}
76+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { createVerification } from "../utils/code_management.ts";
3+
import { Verification } from "../utils/types.ts";
4+
5+
export interface Request {
6+
email: string;
7+
userToken?: string;
8+
}
9+
10+
export interface Response {
11+
verification: Verification;
12+
}
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
const { code, verification } = await createVerification(
21+
ctx,
22+
req.email,
23+
);
24+
25+
// Send email
26+
await ctx.modules.email.sendEmail({
27+
from: {
28+
email: ctx.config.fromEmail ?? "hello@test.com",
29+
name: ctx.config.fromName ?? "Authentication Code",
30+
},
31+
to: [{ email: req.email }],
32+
subject: "Your verification code",
33+
text: `Your verification code is: ${code}`,
34+
html: `Your verification code is: <b>${code}</b>`,
35+
});
36+
37+
return { verification };
38+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { IDENTITY_INFO_PASSWORD } from "../utils/provider.ts";
3+
4+
export interface Request {
5+
email: string;
6+
password: string;
7+
}
8+
9+
export interface Response {
10+
userToken: string;
11+
userId: string;
12+
}
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
// Try signing in with the email
21+
const { userToken, userId } = await ctx.modules.identities.signIn({
22+
info: IDENTITY_INFO_PASSWORD,
23+
uniqueData: {
24+
identifier: req.email,
25+
},
26+
});
27+
28+
// Verify the password
29+
await ctx.modules.userPasswords.verify({
30+
userId,
31+
password: req.password,
32+
});
33+
34+
return { userToken, userId };
35+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Empty, RuntimeError, ScriptContext } from "../module.gen.ts";
2+
import { verifyCode } from "../utils/code_management.ts";
3+
import { IDENTITY_INFO_PASSWORD } from "../utils/provider.ts";
4+
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";
5+
6+
export interface Request {
7+
userToken: string;
8+
9+
email: string;
10+
password: string;
11+
oldPassword: string | null;
12+
13+
verificationToken: string;
14+
code: string;
15+
}
16+
17+
export type Response = Empty;
18+
19+
export async function run(
20+
ctx: ScriptContext,
21+
req: Request,
22+
): Promise<Response> {
23+
await ctx.modules.rateLimit.throttlePublic({});
24+
25+
// Check the verification code. If it is valid, but for the wrong email, say
26+
// the verification failed.
27+
const { email } = await verifyCode(ctx, req.verificationToken, req.code);
28+
if (!compareConstantTime(req.email, email)) {
29+
throw new RuntimeError("verification_failed");
30+
}
31+
32+
// Ensure that the email is not associated with ANY accounts in ANY way.
33+
const providedUser = await ctx.modules.users.authenticateToken({
34+
userToken: req.userToken,
35+
});
36+
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));
37+
38+
// If an old password was provided, ensure it was correct and update it.
39+
// If one was not, register the user with the `userPasswords` module.
40+
if (req.oldPassword) {
41+
await ctx.modules.userPasswords.verify({
42+
userId: providedUser.userId,
43+
password: req.oldPassword,
44+
});
45+
await ctx.modules.userPasswords.update({
46+
userId: providedUser.userId,
47+
newPassword: req.password,
48+
});
49+
} else {
50+
await ctx.modules.userPasswords.add({
51+
userId: providedUser.userId,
52+
password: req.password,
53+
});
54+
}
55+
56+
// Sign up the user with the passwordless email identity
57+
await ctx.modules.identities.link({
58+
userToken: req.userToken,
59+
info: IDENTITY_INFO_PASSWORD,
60+
uniqueData: {
61+
identifier: email,
62+
},
63+
additionalData: {},
64+
});
65+
66+
return {};
67+
}
68+
69+
function compareConstantTime(aConstant: string, b: string) {
70+
let isEq = 1;
71+
for (let i = 0; i < aConstant.length; i++) {
72+
isEq &= Number(aConstant[i] === b[i]);
73+
}
74+
isEq &= Number(aConstant.length === b.length);
75+
76+
return Boolean(isEq);
77+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Empty, ScriptContext } from "../module.gen.ts";
2+
import { verifyCode } from "../utils/code_management.ts";
3+
import { IDENTITY_INFO_PASSWORDLESS } from "../utils/provider.ts";
4+
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";
5+
6+
export interface Request {
7+
verificationToken: string;
8+
code: string;
9+
userToken: string;
10+
}
11+
12+
export type Response = Empty;
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
// Verify that the code is correct and valid
21+
const { email } = await verifyCode(ctx, req.verificationToken, req.code);
22+
23+
// Ensure that the email is not already associated with another account
24+
const providedUser = await ctx.modules.users.authenticateToken({
25+
userToken: req.userToken,
26+
});
27+
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));
28+
29+
// Add email passwordless sign in to the user's account
30+
await ctx.modules.identities.link({
31+
userToken: req.userToken,
32+
info: IDENTITY_INFO_PASSWORDLESS,
33+
uniqueData: {
34+
identifier: email,
35+
},
36+
additionalData: {},
37+
});
38+
39+
return {};
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Empty, ScriptContext } from "../module.gen.ts";
2+
import { verifyCode } from "../utils/code_management.ts";
3+
import { IDENTITY_INFO_LINK } from "../utils/provider.ts";
4+
import { ensureNotAssociatedAll } from "../utils/link_assertions.ts";
5+
6+
export interface Request {
7+
verificationToken: string;
8+
code: string;
9+
userToken: string;
10+
}
11+
12+
export type Response = Empty;
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
// Verify that the code is correct and valid
21+
const { email } = await verifyCode(ctx, req.verificationToken, req.code);
22+
23+
// Ensure that the email is not already associated with another account
24+
const providedUser = await ctx.modules.users.authenticateToken({
25+
userToken: req.userToken,
26+
});
27+
await ensureNotAssociatedAll(ctx, email, new Set([providedUser.userId]));
28+
29+
// Link the email to the user's account
30+
await ctx.modules.identities.link({
31+
userToken: req.userToken,
32+
info: IDENTITY_INFO_LINK,
33+
uniqueData: {
34+
identifier: email,
35+
},
36+
additionalData: {},
37+
});
38+
39+
return {};
40+
}

0 commit comments

Comments
 (0)