SvelteKit JWT Authorization

Hello, this article is about how to implement authentication in your SvelteKit project. This will be JWT authentication using refresh tokens for added security. We will be using Supabase as our database (PostgreSQL), but the basics should be the same.

Github repository

How will it work?

When a user registers, we store the user information and password in our database. We will also generate a refresh token and store it both locally and in the database. We will create a JWT token with user information and save it as cookie. This JWT token will expire in 15 minutes. When it expires, we will check if the refresh token exists and compare it with the one stored in our database. If it matches, we can create a new JWT token. With this system, you can revoke a user’s access to your website by changing the refresh token stored in the database (although this can take up to 15 minutes).

Finally, why Supabase and not Firebase? Personally, I think unlimited read/write is much more important than storage size when running a free system. But any database should work.

I. Structure

This project will consist of 3 pages

  • index.svelte: Project page

  • signin.svelte: Login page

  • signup.svelte: Registration page

Well, the packages that we will use

  • supabase

  • bcrypt: For password hashing

  • crypto: To generate user id (UUID)

  • jsonwebtoken: To create a JWT

  • cookie: For parsing cookie from the server

II. supabase

Create a new project. Now create a new table users (all non-null).

  • id : int8, unique, isIdentity

  • email : varchar, unique

  • password : text

  • username : varchar, unique

  • user_id : uuid, unique

  • refresh_token : text

Go to settings > api. Copy service_role and URL. Create supabase-admin.ts:

import { createClient } from '@supabase/supabase-js';

export const admin = createClient(
    'URL',
    'service_role'
);

If you are using Supabase, DO NOT use this client (admin). Create a new client using your anon key.

III. Creating an account (account)

Create a new endpoint (/api/create-user.ts). He will be for POST request, and as body(bodies) will be required email, password and username.

export const post: RequestHandler = async (event) => {
    const body = (await event.request.json()) as Body;
    if (!body.email || !body.password || !body.username) return returnError(400, 'Invalid request');
    if (!validateEmail(body.email) || body.username.length < 4 || body.password.length < 6)
        return returnError(400, 'Bad request');
}

By the way, returnError() is only meant to make the code cleaner. And validateEmail() just checks if the string has @because (to my knowledge) we can’t 100% check if an email is valid using a regular expression.

export const returnError = (status: number, message: string): RequestHandlerOutput => {
    return {
        status,
        body: {
            message
        }
    };
};

In any case, let’s make sure email or username not yet used.

const check_user = await admin
    .from('users')
    .select()
    .or(`email.eq.${body.email},username.eq.${body.username}`)
    .maybeSingle()
if (check_user.data) return returnError(405, 'User already exists');

Then we hash the user’s password (password) and create a new one user_id (UUID) and a refresh token that will be stored in our database.

const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(body.password, salt);
const user_id = randomUUID();
// import { randomUUID } from 'crypto';
const refresh_token = randomUUID();
const create_user = await admin.from('users').insert([
    {
        email: body.email,
        username: body.username,
        password: hash,
        user_id,
        refresh_token
    }
]);
if (create_user.error) return returnError(500, create_user.statusText);

Finally, generate a new JWT token. Be sure to choose something random for the key. Make sure you have set safe mode (Secure) only if you’re in development mode (localhost is http, not https).

const user = {
    username: body.username,
    user_id,
    email: body.email
};
const secure = dev ? '' : ' Secure;';
// import * as jwt from 'jsonwebtoken';
// expires in 15 minutes
const token = jwt.sign(user, key, { expiresIn: `${15 * 60 * 1000}` });
return {
    status: 200,
    headers: {
        // import { dev } from '$app/env';
        // const secure = dev ? '' : ' Secure;';
        'set-cookie': [
            // expires in 90 days
            `refresh_token=${refresh_token}; Max-Age=${30 * 24 * 60 * 60}; Path=/; ${secure} HttpOnly`,
            `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`
        ]
    }
};

On our registration page, we can call POST-request and redirect our user if successful. Be sure to use window.location.href instead of goto()otherwise change (set cookie) will not be applied.

const signUp = async () => {
    const response = await fetch('/api/create-user', {
        method: 'POST',
        credentials: 'same-origin',
        body: JSON.stringify({
            email,
            username,
            password
        })
    });
    if (response.ok) {
        window.location.href="https://habr.com/";
    }
};

IV. Entrance

We will process the input in /api/signin.ts. This time we will allow the user to use either their username (username) or email address (email). To do this we can check if it is a valid username or email and check if the same username or email exists

export const post: RequestHandler = async (event) => {
    const body = (await event.request.json()) as Body;
    if (!body.email_username || !body.password) return returnError(400, 'Invalid request');
    const valid_email = body.email_username.includes('@') && validateEmail(body.email_username);
    const valid_username = !body.email_username.includes('@') && body.email_username.length > 3;
    if ((!valid_email && !valid_username) || body.password.length < 6)
        return returnError(400, 'Bad request');
    const getUser = await admin
        .from('users')
        .select()
        .or(`username.eq.${body.email_username},email.eq.${body.email_username}`)
        .maybeSingle()
    if (!getUser.data) return returnError(405, 'User does not exist');
}

Next, we will compare the entered and saved password.

const user_data = getUser.data as Users_Table;
const authenticated = await bcrypt.compare(body.password, user_data.password);
if (!authenticated) return returnError(401, 'Incorrect password');

And finally, do the same as when creating a new account.

const refresh_token = user_data.refresh_token;
const user = {
    username: user_data.username,
    user_id: user_data.user_id,
    email: user_data.email
};
const token = jwt.sign(user, key, { expiresIn: `${expiresIn * 60 * 1000}` });
return {
    status: 200,
    headers: {
        'set-cookie': [
            `refresh_token=${refresh_token}; Max-Age=${refresh_token_expiresIn * 24 * 60 * 60}; Path=/; ${secure} HttpOnly`,
            `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`
        ]
    }
};

V. User authentication

Although we can use hooks to read the JWT token (as in this articlewritten by the author), we will not be able to generate (and install) a new JWT token with their help. So, we will call the endpoint, which will read cookie and validate them, and return the user’s data if it exists. This endpoint will also handle update sessions (refreshing sessions). This endpoint will be called /api/auth.ts.

We can get cookie, and if they are valid – return the user’s data. If they are invalid, the function verify() will give an error message.

export const get: RequestHandler = async (event) => {
    const { token, refresh_token } = cookie.parse(event.request.headers.get('cookie') || '');
    try {
        const user = jwt.verify(token, key) as Record<any, any>;
        return {
            status: 200,
            body: user
        };
    } catch {
        // invalid or expired token
    }
}

If the JWT token has expired, we can check the refresh token with the token in our database. If they are equal, then we can create a new JWT token.

if (!refresh_token) return returnError(401, 'Unauthorized user');
const getUser = await admin.from('users').select().eq("refresh_token", refresh_token).maybeSingle()
if (!getUser.data) {
    // remove invalid refresh token
    return {
        status: 401,
        headers: {
            'set-cookie': [
                `refresh_token=; Max-Age=0; Path=/;${secure} HttpOnly`
            ]
        },
    }
}
const user_data = getUser.data as Users_Table;
const new_user = {
    username: user_data.username,
    user_id: user_data.user_id,
    email: user_data.email
};
const token = jwt.sign(new_user, key, { expiresIn: `${15 * 60 * 1000}` });
return {
    status: 200,
    headers: {
        'set-cookie': [
            `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`
        ]
    },
};

VI. User authorization

To authorize the user, we can check if the request was sent from /api/auth in load functions.

// index.sve;te
// inside <script context="module" lang="ts"/>
export const load: Load = async (input) => {
    const response = await input.fetch('/api/auth');
    const user = (await response.json()) as Session;
    if (!user.user_id) {
        // user doesn't exist
        return {
            status: 302,
            redirect: '/signin'
        };
    }
    return {
        props: {
            user
        }
    };
};

VII. User logout

To log out, simply remove the JWT token and the refresh token.

// /api/signout.ts
export const post : RequestHandler = async () => {
    return {
    status: 200,
        headers: {
            'set-cookie': [
                `refresh_token=; Max-Age=0; Path=/; ${secure} HttpOnly`,
                `token=; Max-Age=0; Path=/;${secure} HttpOnly`
            ]
        }
    };
};

VIII. Revoking access from a user

To revoke access from a user, simply change the user’s refresh token in the database. Keep in mind that the user will stay logged in for up to 15 minutes (JWT expiration date).

const new_refresh_token = randomUUID();
await admin.from('users').update({ refresh_token: new_refresh_token }).eq("refresh_token", refresh_token);

These are the basics, but once you get the hang of it, implementing profile updates and other features should be fairly straightforward.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *