Authentication, Nuxt, Strapi··x·x

User Auth and Session Management in Nuxt with Sidebase and Strapi v4

Learn how to implement user authentication and session management in a Nuxt application using Sidebase Nuxt-Auth module and Strapi v4 as the backend.

Welcome

Project setup

  • Nuxt - 3.4.2
  • @sidebase/nuxt-auth - 0.5.0

Important Before you start, read through the main Github page of @sidebase@nuxt-auth. The documentation is great and the team is very helpful.

In this guide, we'll be adding auth and session management features into our Nuxt application and authenticate with Strapi through the @sidebase/nuxt-auth module made by sidebase.

Introduction

Nuxt is a meta framework built on top of Vue.

Strapi is a powerful CMS back-end you can fully customize to your needs. It has a lot of built in creature comforts to start off with.

@sidebase/nuxt-auth is an authentication module built for use with Nuxt. It's easy, fast, and secure.

We will use @sidebase/nuxt-auth for managing the sessions and focus on its own great features like protecting pages globally. You can easily create any amount of users on your front-end through Strapi and really focus only on building out your prototype, without needing to spend hours in setting up every single piece. I work with a full stack mindset and I in my opinion the combination of Strapi, Nuxt and @sidebase/nuxt-auth is just amazing. Spend more time on developing and less on setting up every nitty gritty thing. I like to work smarter, not harder.

The source code for this article is available on GitHub. The orginal nuxt-auth-example template can also be found on Github

Creating a New Strapi project

  1. Run the below command in terminal to start the Strapi installation script.
yarn create strapi-app my-strapi-project --quickstart
// or 
npx create-strapi-app@latest my-strapi-project --quickstart

Once installation is complete, your browser automatically opens a new tab. Strapi runs out of the box on http://localhost:1337/. After installation we need to create a admin user account.

  1. Complete the form on the new tab in order to create your own administrator account.

Once completed, you can use that account to sign into your Strapi application. You will be redirected to the admin panel.

In the admin panel, navigate to Content Manager on the top-left main navigation menu. Once on the Content Manager page click on the User collection-type. The User collection type can hold many application-side user's, we will create a new User record.

  1. In the right top corner, click on Create New Entry.
  2. Fill in all the User fields on this new page and remember the password.
    username: 'The visible name of the user e.g: Juliette.'
    email: 'Email of the user'
    password: 'Generate/create a password'
    blocked: 'Set this to false' 
    confirmed: 'Set this to true'
  1. Save the new User.

We will need to save the User account credentials, because we will need to use either the username or the email of the user together with the password we created here to authenticate in Nuxt.

That's all we have to do within our Strapi project. We will now move on to the Nuxt Application side of things.

Creating a New Nuxt Application

Let's start by creating a new Nuxt application. We can create a Nuxt application using the following command in terminal:

npx nuxi init nuxt-auth-strapi

The above command will ask for the name of the project. We'll call it nuxt-auth-strapi.

Once the setup of the project and installing all dependencies are complete, we can go inside the application-side directory and start the application using the following command:

cd nuxt-auth-strapi && yarn dev

The above command will start the application on http://localhost:3000/.

This article won't be covering the installation of TailwindCSS. Here is a link to the official TailwindCSS docs for Nuxt to help you install and configure that part yourself.

Installing and Integrating @sidebase/nuxt-auth with Nuxt and Strapi

In this section, we'll be installing and integrating nuxt-auth.

  1. Install the package
yarn add --dev @sidebase/nuxt-auth
// or 
npm i -D @sidebase/nuxt-auth
  1. Add @sidebase/nuxt-auth to your modules in nuxt.config.ts file.
export default defineNuxtConfig({
  modules: ['@sidebase/nuxt-auth']
})
  1. Create a .env file in the root of your Nuxt project.
ORIGIN=http://localhost:3000
NUXT_SECRET=<-a-better-secret->,
STRAPI_BASE_URL=http://localhost:1337/api
  1. Add your runtime environment variables, these are private keys that are only available server-side in nitro.
runtimeConfig: { 
    private: {
        NUXT_SECRET: process.env.NUXT_SECRET,
        STRAPI_BASE_URL: process.env.STRAPI_BASE_URL,
    },
    public: {}
}
  1. Add and configure the auth object with the following options.
auth: {
    origin: process.env.ORIGIN,
    }
},

Your nuxt.config.ts file should contain the following code:

export  default  defineNuxtConfig({
  runtimeConfig: { 
      private: {
        // The private keys which are only available server-side
        NUXT_SECRET: process.env.NUXT_SECRET,
        STRAPI_BASE_URL: process.env.STRAPI_BASE_URL,
      },
      public: {}
  },
  modules: [
    "@sidebase/nuxt-auth",
    ],
  auth: {
    origin: process.env.ORIGIN,
  },
});

Make sure you have updated your .env file accordingly.

Creating and Integrating the Strapi Credential Flow into our Nuxt Application with Sidebase

In this section, we'll create a custom Credentials Flow for Strapi and integrate it into Nuxt.js and our Strapi application.

First off the Nuxt-Auth module expects all auth requests to be sent to /api/auth/, all requests will be handled by the NuxtAuthHandler.

  1. You'll need to create a folder in the root of your Nuxt project called server in this server folder you need to create two more folders called api and inside that auth.
     Folders ./server/api/auth/
    
  2. We need to create a catch-all server-route that holds our NuxtAuthHandler, create a file called [...].ts in the auth folder.
  3. Copy the following logic into the newly created [...].ts file.
// ~/server/api/auth/[...].ts
import CredentialsProvider from "next-auth/providers/credentials";
import { NuxtAuthHandler } from "#auth";

export default NuxtAuthHandler({
  // secret needed to run nuxt-auth in production mode (used to encrypt data)
  secret: useRuntimeConfig().private.NUXT_SECRET,
  providers: [
    // @ts-ignore Import is exported on .default during SSR, so we need to call it this way. May be fixed via Vite at some point
    CredentialsProvider.default({
      name: "Credentials",
      credentials: {
      // We need the credentials object to be present.
      // You can leave it empty though.
      },
      async authorize(credentials: any) {
        
        const response = await $fetch(
          `${useRuntimeConfig().private.STRAPI_BASE_URL}/api/auth/local/`,
          {
            method: "POST",
            body: JSON.stringify({
              identifier: credentials.username,
              password: credentials.password,
            }),
          }
        );

        if (response.user) {
          const u = {
            id: response.id,
            name: response.user.username,
            // Passing the original JWT through the email field.
            // IMPORTANT: Do not pass decoded JWTs, 
            // always make sure they are encoded!
            email: response.jwt
          };
          return u;
        } else {
          throw  new  Error('User not found');
        }
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin'
  },
});

Note: We feed the original JWT token returned by Strapi in the email field of the Nuxt-Auth user object. This is a separate and internally managed session through a different JWT. Don't use that JWT because that will not work.

The above code is a custom credentials provider that talks with Strapi's local auth flow. You can completely replace the provider and add a lot more options like callbacks to improve this example case.

Sign-in Page & Auth Logic Setup

Custom sign-in page

In the pages folder create a new folder called auth and add a new vue page called signin.vue.

  1. Copy the following template code
<script setup lang="ts">
const { signIn, signOut, data, status, getSession, getCsrfToken, getProviders } = await useAuth({ required: false })

const providers = await getProviders()
const crsf = await getCsrfToken()

let credentials = reactive({
  username: "",
  password: "",
});

const logIn = async (e) => {
    e.preventDefault() 
    await signIn('credentials', { callbackUrl: '/protected', redirect: true, username: loginForm.username, password: loginForm.password } )
};
</script>

<template>
  <section class="grid grid-cols-2 wrapper my-6 mx-auto items-center justify-center">
    <!-- login container -->
    <div class="bg-gray-100 flex mx-auto rounded-2xl ease-in-out duration-300 shadow-xl max-w-[60rem] p-5 items-center">
      <!-- form -->
      <div class=" px-8 md:px-16">
        <h2 class="font-bold text-2xl text-[#002D74]">Login</h2>
        <p class="text-xs mt-4 text-[#002D74]">Strapi users can login with their username (or e-mail) and password.</p>
        
        <form class="flex flex-col gap-4">
          <input name="csrfToken" type="hidden" v-model="credentials.csrfToken" />
          <input v-model="credentials.username" class="p-2 mt-8 rounded-xl border" type="text" name="email" placeholder="Email">
          <div class="relative">
            <input v-model="credentials.password" class="p-2 rounded-xl border w-full" type="password" name="password" placeholder="Password">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="gray" class="bi bi-eye absolute top-1/2 right-3 -translate-y-1/2" viewBox="0 0 16 16">
              <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" />
              <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" />
            </svg>
          </div>

          <button class="bg-[#002D74] rounded-xl text-white py-2 hover:scale-105 duration-300" @click="logIn">
            Sign in (Credential Flow)
          </button>
        </form>
      </div>
    </div>
    
    <div class="max-w-5xl mx-auto mt-5 px-5">
        <h3 class="text-xl font-bold ">Authentication Overview</h3>
        <pre v-if="status"><span>Status:</span> {{ status }}</pre>
        <pre v-if="data"><span>Data:</span> {{ data }}</pre>
        <pre v-if="providers"><span>Providers:</span> {{ providers }}</pre>
    </div>
  </section>
</template>

@sidebase/nuxt-auth normally generates it's own login page with matching provider styles. For this example I am sending the input field values through the signIn() helper called from within our custom ./pages/auth/signin page.

You can directly attach the signIn() function to the submit button.

@click="signIn('credentials', { callbackUrl: '/protected/globally', username: credentials.username, password: credentials.password})"

Alternatively, we can write a simple function that allows us to also log the response. In the example I've changed the previous example to call the logIn() function instead, to help you 'wrap' your head around it.

const logIn = async (e) => {
    e.preventDefault() 
    const res = await  signIn('credentials', { callbackUrl: '/protected/globally', redirect: true, username: loginForm.username, password: loginForm.password } )
    // Will either print status 200 on success or the error you set up in the [...].ts nitro endpoint. 
    console.log(res)
};

Setting up middleware

Application Side Middleware

  1. Create a folder called middleware in the root of your app.
  2. Create a file in the middleware folder called auth.global.ts and add the following code
import { defineNuxtRouteMiddleware } from  '#app'

export  default  defineNuxtRouteMiddleware(async  (to)  => {
if (to.path.startsWith('/protected/')) {
    await  useAuth({ callbackUrl: to.path })
    }
})

Setting up Nitro for Authenticated Strapi requests.

After logging in with Strapi User we can use the returned JWT from Strapi to access the authenticated endpoints. Lets a create a new endpoint first and then add the JWT when we call it on our signin page.

Nitro & Strapi Auth Endpoint Access

Create a new nitro server endpoint

  1. In the server/api folder create a new folder called admin
    ./server/api/admin
  2. Create a new server route called data.get.ts in the admin folder. Copy the following code
export default defineEventHandler(async (event) => {
  const settings = {
    method: "GET",
    headers: {
      "Content-Type": "application/json"
    },
  };

  const response = await $fetch(
    `${useRuntimeConfig().private.STRAPI_BASE_URL}/api/<your-strapi-content-endpoint>`,
    settings
  );
  return response;
});

First we need to provide Nitro with the application-side fetch function together with session JWT header

We will add some logic to the sign-in page that passes the session JWT headers to Nitro.

  1. Copy the following code and add it in the script setup part of your signin page.
// file: ./pages/auth/signin.vue
const headers = useRequestHeaders(['cookie'])
await $fetch(`/api/admin/data`, {
  method: "GET",
  headers: { cookie: headers.cookie }
});

Grab the session JWT with getToken()

In the data.get.ts server route.

  1. Import the 'getToken' helper.
import { getToken } from "#auth";
  1. Inside the defineEventHandler add:
 const token = await getToken({ event });

Add the original Strapi JWT to your Authorisation header

  1. Modify the Authorisation header to include the JWT token from the token.
Authorization: "Bearer " + token.email

The entire Auth server endpoint.

The final code in /server/api/admin/data.get.ts.

import { getToken, getServerSession } from "#auth";

export default defineEventHandler(async (event) => {
    const  session  =  await  getServerSession(event);
    if (!session) return { status: "unauthenticated!" };  
    
    const token = await getToken({ event });
    const settings = {
        method: "GET",
        headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + token.email
        },
  };

  const response = await $fetch(
    `${useRuntimeConfig().private.STRAPI_BASE_URL}/api/<your-strapi-auth-protected-endpoint>`,
    settings
  );
  return response;
});

Change log

Thank you for the help BracketJohn, helping me work out the kinks of the article!

Update 17-11-22 The Nuxt-Auth module is released out of beta. I added syntax highlighting, @tailwind/typography doesn't support this out of the box so had to work my away around...

Update 22-11-22

  • Updated to Nuxt stable production release.
  • Optimised and improved the codebase.
  • Updated the article to reflect the changes in my example repo

Update 24-11-22

  • Updated to the latest version of nuxt-auth.
  • Optimised and improved the codebase by adding the Strapi setup.
  • Updated the article to reflect the changes in my example repo
  • Trying to fix the vue syntax highlighting ASAP...

Update 25-04-23

  • Updated to the latest version of nuxt-auth.
  • Updated to use useAuth() instead of useSession().
  • Updated all imports
  • Fixed the vue syntax highlighting ˆˆ.

Services

Copyright © 2024. All rights reserved.