diff --git a/docs/pages/guides/email-and-password/email-verification-codes.md b/docs/pages/guides/email-and-password/email-verification-codes.md new file mode 100644 index 000000000..e52e0ee34 --- /dev/null +++ b/docs/pages/guides/email-and-password/email-verification-codes.md @@ -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 { + 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() + } + }); +}); +``` diff --git a/docs/pages/guides/email-and-password/email-verification-links.md b/docs/pages/guides/email-and-password/email-verification-links.md index 17013eeeb..f449d6cc4 100644 --- a/docs/pages/guides/email-and-password/email-verification-links.md +++ b/docs/pages/guides/email-and-password/email-verification-links.md @@ -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"; @@ -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 @@ -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); } diff --git a/docs/pages/guides/email-and-password/email-verification.md b/docs/pages/guides/email-and-password/email-verification.md deleted file mode 100644 index 29b9a9111..000000000 --- a/docs/pages/guides/email-and-password/email-verification.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "Email verification" ---- - -# Email verification - -``` -still working on this one! -``` diff --git a/docs/pages/guides/email-and-password/index.md b/docs/pages/guides/email-and-password/index.md index 1c160f360..c360c5ed4 100644 --- a/docs/pages/guides/email-and-password/index.md +++ b/docs/pages/guides/email-and-password/index.md @@ -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)