Lucia v3

Two-factor authorization

The guide covers how to implement two-factor authorization using time-based OTP (TOTP) and authenticator apps.

Update database

Update the user table to include two_factor_secret column. You can of course store the secret in its own table.

import { Lucia } from "lucia";

export const lucia = new Lucia(adapter, {
	sessionCookie: {
		attributes: {
			secure: env === "PRODUCTION" // set `Secure` flag in HTTPS
		}
	},
	getUserAttributes: (attributes) => {
		return {
			// ...
			// don't expose the secret
			// rather expose whether if the user has setup 2fa
			setupTwoFactor: attributes.two_factor_secret !== null
		};
	}
});

declare module "lucia" {
	interface Register {
		Lucia: typeof lucia;
		DatabaseUserAttributes: {
			two_factor_secret: string | null;
		};
	}
}

Create QR code

When the user signs up, set two_factor_secret to null to indicate the user has yet to set up two-factor authorization.

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

	const userId = generateIdFromEntropySize(10);

	await db.table("user").insert({
		id: userId,
		two_factor_secret: null
		// ...
	});

	// ...
});

Generate a new secret (minimum 20 bytes) and create a new key URI with createTOTPKeyURI(). The user should scan the QR code using their authenticator app.

import { encodeHex } from "oslo/encoding";
import { createTOTPKeyURI } from "oslo/otp";

const { user } = await lucia.validateSession(sessionId);
if (!user) {
	return new Response(null, {
		status: 401
	});
}

const twoFactorSecret = crypto.getRandomValues(new Uint8Array(20));
await db
	.table("user")
	.where("id", "=", user.id)
	.update({
		two_factor_secret: encodeHex(twoFactorSecret)
	});

// pass the website's name and the user identifier (e.g. email, username)
const uri = createTOTPKeyURI("my-app", user.email, twoFactorSecret);

// use any image generator
const qrcode = createQRCode(uri);

Validate OTP

Validate TOTP with TOTPController using the stored user's secret.

import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp";

let otp: string;

const { user } = await lucia.validateSession(sessionId);
if (!user) {
	return new Response(null, {
		status: 401
	});
}

const result = await db.table("user").where("id", "=", user.id).get("two_factor_secret");
const validOTP = await new TOTPController().verify(otp, decodeHex(result.two_factor_secret));