Skip to content

Commit

Permalink
add email verification code guide
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper committed Dec 26, 2023
1 parent c1e6427 commit 7be0c6a
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 26 deletions.
171 changes: 171 additions & 0 deletions docs/pages/guides/email-and-password/email-verification-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: "Email verification codes"
---

# Email verification codes

## Update database

### User table

Add a `email_verified` column (boolean).

```ts
import { Lucia } from "lucia";

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: env === "PRODUCTION" // set `Secure` flag in HTTPS
}
},
getUserAttributes: (attributes) => {
return {
emailVerified: attributes.email_verified,
email: attributes.email
};
}
});

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
interface DatabaseUserAttributes {
email: string;
email_verified: boolean;
}
}
```

### Email verification code table

Create a table for storing for email verification codes.

| column | type | attributes |
| ------------ | -------- | ------------------- |
| `id` | any | auto increment, etc |
| `code` | `string` | |
| `user_id` | `string` | unique |
| `email` | `string` | |
| `expires_at` | `Date` | |

## Generate verification code

The code should be valid for few minutes and linked to a single email.

```ts
import { TimeSpan, createDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/random";

async function generateEmailVerificationCode(userId: string, email: string): Promise<string> {
await db.table("email_verification_code").where("user_id", "=", userId).deleteAll();
const code = generateRandomString(8, alphabet("0-9"));
await db.table("email_verification_code").insert({
user_id: userId,
email,
code,
expires_at: createDate(new TimeSpan(5, "m")) // 5 minutes
});
return code;
}
```

You can also use alphanumeric codes.

```ts
const code = generateRandomString(6, alphabet("0-9", "A-Z"));
```

When a user signs up, set `email_verified` to `false`, create and send a verification code, and create a new session.

```ts
import { generateId } from "lucia";
import { encodeHex } from "oslo/encoding";

app.post("/signup", async () => {
// ...

const userId = generateId();

await db.table("user").insert({
id: userId,
email,
hashed_password: hashedPassword,
email_verified: false
});

const verificationCode = await generateEmailVerificationCode(userId, email);
await verificationCode(email, verificationCode);

const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize()
}
});
});
```

When resending verification emails, make sure to implement rate limiting based on user ID and IP address.

## Verify code and email

**Make sure to implement throttling to prevent brute-force attacks**.

Validate the verification code by comparing it against your database and checking the expiration and email. Make sure to invalidate all user sessions.

```ts
import { isWithinExpiration } from "oslo";

app.post("/email-verification", async () => {
// ...
const { user } = await lucia.validateSession(sessionId);
if (!user) {
return new Response(401);
}
const code = formData.get("code");
// check for length
if (typeof code !== "string" || code.length !== 8) {
return new Response(400);
}

await db.beginTransaction();
const databaseCode = await db
.table("email_verification_code")
.where("user_id", "=", user.id)
.get();
if (databaseCode) {
await db.table("email_verification_code").where("id", "=", databaseCode.id).delete();
}
await db.commit();

if (!databaseCode || databaseCode.code !== code) {
return new Response(400);
}
if (!isWithinExpiration(databaseCode.expires_at)) {
return new Response(400);
}
if (!user || user.email !== databaseCode.email) {
return new Response(400);
}

await lucia.invalidateUserSessions(user.id);
await db.table("user").where("id", "=", user.id).update({
email_verified: true
});

const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize()
}
});
});
```
21 changes: 8 additions & 13 deletions docs/pages/guides/email-and-password/email-verification-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ title: "Email verification links"

# Email verification links

We recommend using [email verification codes]() instead as it's more user-friendly.
We recommend using [email verification codes](/guides/email-and-password/email-verification-codes) instead as it's more user-friendly.

## Update database

### User table

Add a `email_verified` column. Keep in mind that some database don't supported booleans.
Add a `email_verified` column (boolean).

```ts
import { Lucia } from "lucia";
Expand Down Expand Up @@ -110,16 +110,16 @@ When resending verification emails, make sure to implement rate limiting based o

## Verify token and email

Extract the email verification token from the URL and validate by checking the expiration date and email. If the token is valid, invalidate all existing user sessions and create a new session.
Extract the email verification token from the URL and validate by checking the expiration date and email. If the token is valid, invalidate all existing user sessions and create a new session. Make sure to invalidate all user sessions.

```ts
import { isWithinExpiration } from "oslo";

app.get("email-verification/*", async () => {
app.get("email-verification/:token", async () => {
// ...

// there are better ways to do this - check your framework's API
const verificationToken = request.url.replace("http://localhost:3000/email-verification/", "");
// check your framework's API
const verificationToken = params.token;

await db.beginTransaction();
const token = await db
Expand All @@ -128,17 +128,12 @@ app.get("email-verification/*", async () => {
.get();
await db.table("email_verification_token").where("id", "=", verificationToken).delete();
await db.commit();
if (!token) {
return new Response(400);
}
if (!isWithinExpiration(token.expires_at)) {
await db.table("email_verification_token").where("id", "=", token.id).delete();

if (!token || !isWithinExpiration(token.expires_at)) {
return new Response(400);
}

const user = await db.table("user").where("id", "=", token.user_id).get();
if (!user || user.email !== token.email) {
await db.table("email_verification_token").where("id", "=", token.id).delete();
return new Response(400);
}

Expand Down
9 changes: 0 additions & 9 deletions docs/pages/guides/email-and-password/email-verification.md

This file was deleted.

8 changes: 4 additions & 4 deletions docs/pages/guides/email-and-password/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ title: "Email and password"

# Email and password

Email based auth requires a lot of components so be prepared to do some work!

For a step-by-step, framework specific tutorial, see the [Username and password](/tutorials/username-and-password) tutorial. Keep in mind that the tutorial only covers the basics and you still need to implement email verification among other things.
Email based auth requires a lot of components so be prepared to do some work! For a step-by-step, framework specific tutorial to learn the basics of password based auth and Lucia, see the [Username and password](/tutorials/username-and-password) tutorial.

- [Password basics](/guides/email-and-password/basics)
- [Email verification links](/guides/email-and-password/email-verification-links)
- Email verification
- [Email verification codes](/guides/email-and-password/email-verification-codes) (preferred)
- [Email verification links](/guides/email-and-password/email-verification-links)
- [Password reset](/guides/email-and-password/password-reset)
- [Login throttling](/guides/email-and-password/login-throttling)
- [Two-factor authorization](/guides/email-and-password/2fa)

0 comments on commit 7be0c6a

Please sign in to comment.