SvelteKit Authentication with JWT and Prisma

User Authentication made easy!

March 18 2023

Looking to secure your Svelte website and protect user data? SvelteKit authentication is key. By requiring unique log-in credentials, you can prevent fraudulent accounts and malicious attacks. In this blog post, we'll explore how you can use JWT and Prisma to add authentication in your web development projects. Let's dive in!

Intro to Authentication and Authorization

What is Authentication?

Authentication is the process of verifying the identity of a user or device attempting to access a system or network. This is typically done through the use of usernames and passwords, biometric identification, or security tokens. The goal of authentication is to prevent unauthorized access to sensitive data and resources.

Authentication vs Authorization

Authorization is closely related to authentication, as it depends on the successful completion of the authentication process. Once a user or device has been authenticated and their identity has been established, the authorization process determines what level of access that user or device is granted. The level of authorization granted will depend on factors such as the user's role or permissions, the sensitivity of the data being accessed, and any other security policies that are in place.

What is JWT?

JWT stands for JSON Web Token, a compact and self-contained way to transmit information between parties as a JSON object. It is commonly used for authentication and authorization purposes in web applications. JWTs consist of three parts: a header, a payload, and a signature, which are encoded and combined to form a token that can be easily passed between parties. The contents of the token can be verified and trusted, as the signature ensures the integrity and authenticity of the data.

User Model

The first step in building an authentication system is having a way to store user's credentials and information. For this article we'll be using Prisma, a popular JavaScript ORM.

prisma.schema

model User {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  password  String
}

Here the most important fields are email and password which will be used for authentication.

User Sign Up

Form

With the user model created we need a way for users to input their email and password for account creation. To do this we can use a standard html form.

user-auth/register/+page.svelte

<h1>User Auth Register</h1>
<form method="POST" action="?/register">
    <input type="email" name="email" placeholder="Email Address"/>
    <input type="password" name="password" placeholder="Password"/>
    <button type="submit">Register</button>
</form>

Form Action

To handle what happens when a user enters their information in the Sign Up form we'll be using SvelteKit's Form Actions where we can take the user's input, create a user object on our database and store the JWT we create.

user-auth/register/+page.server.js

export const actions = {
  register:  async ({cookies, request}) => {
    const formData = Object.fromEntries(await request.formData());
    const {email, password} = formData;

    const {error, token} = await createUser(email, password);

    if (error) {
      console.log({error});
      return fail(500, {error});
    }

    setAuthToken({cookies, token});

    throw  redirect(302, "/user-auth");
  }
}

User Creation

In the previous code snippet the function createUser (defined below) is used to take the email and password provided by the user and create a database entry for the user.

user.js

export async function createUser(email, password) {
  try {
    const user = await db.user.create({
      data: {
        email,
        password: await bcrypt.hash(password, 12)
      }
    });

    const token = createJWT(user);

    return {token};
  } catch (error) {
    return error;
  }
}

In createUser we handle encrypting the password with the bcryptjs package and creating a JWT. Encrypting the password when inserting it into the database is critical since if for some reason our website was hacked and the database was leaked, this would prevent the passwords from being easily viewable.

Handling JWT

Creating

In the above snippet we call createJWT which is defined below.

user.js

function createJWT(user) {
  return  jwt.sign({id: user.id, email: user.email}, JWT_ACCESS_SECRET, {
    expiresIn: '1d'
  });
}

Here we use the jsonwebtoken authentication library to create a new JWT. Putting the user's id and email in the JWT is necessary for when we decrypt the token and lookup the user in the database upon subsequent network requests by this user. The JWT_ACCESS_SECRET should be stored securely in the .env file and is used to ensure that the user can't tamper with the JWT.

Storing

Once we've created the JWT in createUser we need to store the token on the user's browser so that it can be included in future requests. Since we already have access to the cookies in the form action, this is a natural place to store the token.

helpers.js

export const setAuthToken = ({cookies, token}) => {
  cookies.set('AuthorizationToken', `Bearer ${token}`, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24,
    path: '/'
  });
};

For more information on what these parameters mean, read through the MDN docs.

Including in Requests

Now that the JWT is stored in the user's cookies, it needs to be included in subsequent requests to the website. To ensure this happens we will be using SvelteKit's hooks.

hooks.server.js

export const handle = async ({event, resolve}) => {
  const authCookie = event.cookies.get('AuthorizationToken');

  if (authCookie) {
    const token = authCookie.split(' ')[1];
    try {
      const jwtUser = jwt.verify(token, JWT_ACCESS_SECRET);
      const user = await db.user.findUnique({
        where: {
          id: jwtUser.id
        },
        select: {
          id: true,
          email: true,
        }
      });
      if (user) {
        event.locals.user = user;
      }
    } catch (error) {
      console.log(error);
    }
  }
  return await resolve(event);
};

Here we read the JWT out of the cookies, verify that it hasn't been tampered by the user and use the contents of the token to find the user record in the database. If the user is found, we store their information in the event locals for later use.

Displaying User on Client

With the user's info being included in the event locals we can use SvelteKit's load function concept to bring the user's info to the client.

user-auth/+page.server.js

export const load = async ({locals}) => {
  const user = locals.user;
  return {user};
};
user-auth/+page.svelte.js

<script>
    export let data;
</script>

<h1>User Auth</h1>

{#if data?.user}
    <h1>Logged in as user: {data?.user?.email}</h1>
    <form method="POST" action="?/logout">
        <button type="submit">Log Out</button>
    </form>
{:else}
    <a href="user-auth/login">Login</a>
    <a href="user-auth/register">Register</a>
{/if}

In the first snippet we bring the user's info to the client and in the second snippet we check if there is a user logged in and show appropriate html. You may have also noticed that there is a logout form which we will address next.

User Log Out

To log out the user we simply need to delete the JWT from the user's cookies and redirect the user to a non-authorized page.

user-auth/+page.server.js

export const actions = {
  logout: async ({cookies}) => {
    cookies.delete("AuthorizationToken");
    throw  redirect(302, "/user-auth");
  }
}

User Log In

Form

Now that we have the full authentication flow for first time users, we need to handle return users that have logged out and need to log in.

user-auth/login/+page.svelte

<h1>User Auth Login</h1>
<form method="POST" action="?/login">
    <input type="email" name="email" placeholder="Email Address" />
    <input type="password" name="password" placeholder="Password"/>
    <button type="submit">Log In</button>
</form>

Form Action

To handle user input we will once again use form actions similarly to how we handled user sign up.

user-auth/login/+page.server.js

export const actions = {
  login: async ({cookies, request}) => {
    const formData = Object.fromEntries(await request.formData());
    const {email, password} = formData;

    const {error, token} = await loginUser(email, password);

    if (error) {
      return fail(500, {error});
    }

    setAuthToken({cookies, token});

    throw redirect(302, "/user-auth")
  }
}

Here we do basically the same thing that we did in the sign up form action, except instead of creating a new user we are logging in an existing user.

Database

In the previous snippet we called loginUser which we define below.

user.js

export async function loginUser(email, password) {
  try {
    const user = await db.user.findUnique({
      where: {
        email
      }
    });

    if (!user) {
      return {error: 'User not found'};
    }

    const valid = await bcrypt.compare(password, user.password);

    if (!valid) {
      return {error: 'Invalid password'};
    }

    const token = createJWT(user);

    return {token};
  } catch (error) {
    return error;
  }
}

Instead of creating a new user in the database like we did when signing up, we use the email provided by the user to search in the database for a corresponding entry. If a user is found, we use bcryptjs to compare the password provided to the one stored in the database. If the passwords match, a new JWT is created and returned where it can be saved to the user's cookies.

Conclusion

In this article we went over how to handle user authentication in SvelteKit with JWT and Prisma. We went over the entire sign up and log in flow from html forms to SvelteKit's form actions to storing in a database with Prisma. Next steps could include handling a refresh token and adding authorization to protected endpoints.