Back to overview

Setting up authentication in Astro with Prisma and Planetscale

| 4 min read

I’ve been wanting to add authentication to my personal website for a while now to see how it works in Astro. With Prisma and PlanetScale already running for comments on my blogs, I decided to store my account information in PlanetScale. Because it’s just used for my own account, and I’m not storing any other sensitive information in my database, I decided to store the credentials in plain text for now. I changed my Prisma schema to make this possible:

// schema.prisma

model Account {
  id Int @id @default(autoincrement())
  username String @unique
  password String
}

Once the model is updated in the code, running npx prisma db push propagates the changes to PlanetScale, so the schema is updated in the actual database.

I used an existing package called @astro-auth to handle all the authentication on my site. For this to work, I needed to add 2 environment variables to my application: ASTROAUTH_URL (the URL my site is hosted on) and ASTROAUTH_SECRET (a self chosen secret key).

Because I stored the credentials in PlanetScale, I needed to use the CredentialProvider to enable logging in with username and password. There are many other providers available on @astro-auth, go check out the package if you’re interested. The code needed to set this up with @astro-auth looks like this:

// /pages/api/auth/[...astroauth].ts

import AstroAuth from '@astro-auth/core';
import { CredentialProvider } from '@astro-auth/providers';
import { prisma } from '../../../lib/prisma';

export const all = AstroAuth({
  authProviders: [
    CredentialProvider({
      authorize: async properties => {
        const account = await prisma?.account.findFirst({
          where: {
            username: properties.username,
            AND: {
              password: properties.password,
            },
          },
          select: {
            id: true,
          },
        });
        if (account?.id) {
          return properties.username;
        }
        return null;
      },
    }),
  ],
});

Creating a login page was very easy. I just created a form, calling the signIn() method from @astro-auth on submit, and BOOM: logged in! The code for the login page:

// /pages/login.astro

<html>
  <head>
    <title>Login</title>
    <script>
      import { signIn } from '@astro-auth/client';

      document.addEventListener('DOMContentLoaded', () => {
        document.querySelector('form')?.addEventListener('submit', async e => {
          e.preventDefault();
          const form = e.target;
          if (form) {
            const formData = new FormData(form as HTMLFormElement);
            const data = Object.fromEntries(formData);
            await signIn({
              provider: 'credential',
              login: data,
            });
            window.location.href = '/';
          }
        });
      });
    </script>
  </head>
  <body>
    <form>
      <label for="username">Name</label>
      <input type="text" name="username" />

      <label for="password">Password</label>
      <input type="password" name="password" />

      <input type="submit" value="Submit" />
    </form>
  </body>
</html>

After submitting the form, the user’s signed in and is redirected to the homepage. Protecting a page with authentication is easy, just checking the logged in user with the getUser() function from @astro-auth. Here’s an example of a page where I used this check:

// /pages/comment-overview.astro

import { getUser } from '@astro-auth/core'; import Layout from
'../layouts/Layout.astro'; import { prisma } from '../lib/prisma'; import
CommentsOverviewWrapper from '../components/CommentOverviewWrapper'; const user
= getUser({ client: Astro }); if (!user) { return Astro.redirect('/', 307); }
const commentsWithPost = await prisma?.comment.findMany({ include: { post: {
select: { url: true, }, }, }, });

<Layout
  description="Overview of comments"
  title="Lionel Tchami | Comment overview"
>
  <CommentsOverviewWrapper commentsWithPost={commentsWithPost} client:load />
</Layout>

If the user is not logged in, the user will be redirected to the homepage with a 307 status code. I also have an API route to delete comments on my blog posts, which I want to fence off so only authenticated user can use this API. It’s possible to use the getUser() function from @astro-auth for this too, but this time we’re going to pass the request instead of the Astro object. Example of using this code:

// /pages/api/comments.ts

export const del: APIRoute = async ({ request }) => {
  const user = getUser({ server: request });
  if (user) {
    const body = await request.json();
    const deleteComment = await prisma?.comment.delete({
      where: {
        id: body.id,
      },
    });
    return new Response(
      JSON.stringify({
        message: `Comment with id ${deleteComment?.id} deleted`,
      }),
      { status: 200 }
    );
  }
  return new Response(null, { status: 403 });
};

So when the user is not authenticated, a 403 response will be returned.

Hope this was helpful! Source code can be found on my Github as always.

Did you like this post? Check out my latest blog posts:

Maven on EC2
28 Jul 2023

Installing Maven on Ec2 Instance

  • aws
  • maven
  • cloud
Complete CICD Pipeline Java with Jenkins, Nexus, Sonar and AWS services
27 Jul 2023

From Code to Deployment - A Complete CICD journey for Java Apps using Jenkins, Nexus, Sonarqube, AWS ECR & ECS

  • devops
  • aws
  • cicd
Person typing on a laptop
21 Jul 2023

🏗️How to Make Animated ✨GIFs✨ For Amazon Web Services (AWS) Architectures:🏗️ A Step-by-Step Tutorial🏗️

  • aws
  • powerpoint
  • infra
  • resume