DEV Community

pilcrowOnPaper
pilcrowOnPaper

Posted on

SvelteKit + Lucia: Email and password authentication

I recently released Lucia, an authentication library for SvelteKit. It handles the bulk of the authentication process and makes it easier to implement authentication to your SvelteKit project. Unlike other libraries, it isn't an out-of-the-box authentication library and requires a bit more time to start, but can be fully customized once you know the basics. It doesn't just create tokens, it uses short-lived access tokens with refresh tokens, auto-refreshes them, implements rotating refresh token, and detects refresh token theft.

This guide will cover how to implement email and password authentication, at least the sign in part of it. I'm going to gloss over some parts that are better explained in the documentation.

Setup

The basic set up is explained here.

Database

Refer to adapters for database-specific explanation. Add a new column called email (text/varchar) where the values are unique.

Frontend

Send a POST request to the signup or login endpoint with a JSON body.

const signup = async () => {
    await fetch("/api/signup", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
        }),
    });
};
Enter fullscreen mode Exit fullscreen mode

Sign up

Create routes/api/signup.ts and accept a POST request. Get the email and password from the body data.

import type { RequestHandler } from "@sveltejs/kit";
import { auth } from "$lib/lucia";

export const POST: RequestHandler = async ({ request }) => {
    const { email, password } = await request.json();
    if (!email || !password) {
        return {
            status: 400,
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Create a new user

Create a new user using createUser using the auth id of email and an identifier of email. Auth ids represent the authentication method used (for Github OAuth, "github"), and identifier represents the user within that method (usernames or email for GitHub). It's used to identify users and not to store user data (if you need to store the user's email, create a email column in your db).
Refer to the overview page for an explanation on auth ids and identifiers.

Save the user's password using the password options and the user's email using user_data. Set the cookies (refresh, access, and fingerprint token) and redirect the user in the response. The AUTH_DUPLICATE_IDENTIFER_TOKEN error is thrown when a user tries to create a new account using the same auth id and identifer (in this case, email).

try {
    const createUser = await auth.createUser("email", email, {
        password,
        user_data: {
            email,
        },
    });
    return {
        status: 302,
        headers: {
            "set-cookie": createUser.cookies,
            location: "/",
        },
    };
} catch (e) {
    const error = e as Error;
    if (
        error.message === "AUTH_DUPLICATE_IDENTIFER_TOKEN" ||
        error.message === "AUTH_DUPLICATE_USER_DATA"
    ) {
        return {
            status: 400,
            body: JSON.stringify({
                error: "Email already in use.",
            }),
        };
    }
    return {
        status: 500,
        body: JSON.stringify({
            error: "Unknown error.",
        }),
    };
}
Enter fullscreen mode Exit fullscreen mode

Sign in

Create routes/api/login.ts and accept a POST request. Get the user's email and password from the body.

import type { RequestHandler } from "@sveltejs/kit";
import { auth } from "$lib/lucia";

export const POST: RequestHandler = async ({ request }) => {
    const form = await request.formData();
    const email = form.get("email")?.toString();
    const password = form.get("password")?.toString();
    if (!email || !password) {
        return {
            status: 400,
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Authenticate a user

Authenticate the user using authenticateUser, which will require a password in the third parameter. The auth id should be the same as the one used when creating the user. It's important to NOT tell the user if the email was incorrect or if the password was incorrect.

try {
    const authenticateUser = await auth.authenticateUser(
        "email",
        email,
        password
    );
    return {
        status: 302,
        headers: {
            "set-cookie": createUser.cookies,
            location: "/",
        },
    };
} catch (e) {
    const error = e as Error;
    if (
        error.message === "AUTH_INVALID_IDENTIFIER_TOKEN" ||
        error.message === "AUTH_INVALID_PASSWORD"
    ) {
        return {
            status: 400,
            body: JSON.stringify({
                error: "Incorrect email or password.",
            }),
        };
    }
    // database connection error
    return {
        status: 500,
        body: JSON.stringify({
            error: "Unknown error.",
        }),
    };
}
Enter fullscreen mode Exit fullscreen mode

That's it. Other parts of Lucia and authentication (like token refresh and protected routes) are explained in getting started.

Top comments (0)