Lucia

Password reset

Allow users to reset their password by sending them a reset link to their inbox.

Update database

Create a table for storing for password reset tokens.

column type attributes
id string primary key
user_id string
expires_at Date

Create verification token

The token should be valid for at most few hours.

import { TimeSpan, createDate } from "oslo";
import { generateId } from "lucia";

async function createPasswordResetToken(userId: string): Promise<string> {
	// optionally invalidate all existing tokens
	await db.table("password_reset_token").where("user_id", "=", userId).deleteAll();
	const tokenId = generateId(40);
	await db.table("password_reset_token").insert({
		id: tokenId,
		user_id: userId,
		expires_at: createDate(new TimeSpan(2, "h"))
	});
	return tokenId;
}

When a user requests a password reset email, check if the email is valid and create a new link.

import { generateId } from "lucia";

app.post("/reset-password", async () => {
	let email: string;

	// ...

	const user = await db.table("user").where("email", "=", email).get();
	if (!user || !user.email_verified) {
		return new Response("Invalid email", {
			status: 400
		});
	}

	const verificationToken = await createPasswordResetToken(userId);
	const verificationLink = "http://localhost:3000/reset-password/" + verificationToken;

	await sendPasswordResetToken(email, verificationLink);
	return new Response(null, {
		status: 200
	});
});

Make sure to implement rate limiting based on IP addresses.

Verify token

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

import { isWithinExpirationDate } from "oslo";
import { Argon2id } from "oslo/password";

app.post("/reset-password/:token", async () => {
	let password = formData.get("password");
	if (typeof password !== "string" || password.length < 8) {
		return new Response(null, {
			status: 400
		});
	}
	// check your framework's API
	const verificationToken = params.token;

	// ...

	await db.beginTransaction();
	const token = await db.table("password_reset_token").where("id", "=", verificationToken).get();
	if (token) {
		await db.table("password_reset_token").where("id", "=", verificationToken).delete();
	}
	await db.commit();

	if (!token || !isWithinExpirationDate(token.expires_at)) {
		return new Response(null, {
			status: 400
		});
	}

	await lucia.invalidateUserSessions(user.id);
	const hashedPassword = await new Argon2id().hash(password);
	await db.table("user").where("id", "=", user.id).update({
		hashed_password: hashedPassword
	});

	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()
		}
	});
});