AdonisJS 5 API & Nuxt 3 SSR Authentication in 15 Minutes

In this lesson, we’ll learn how to set up authentication in an AdonisJS API application while using server-side rendered (SSR) Nuxt 3 as our front end.

Published
Jun 25, 23
Duration
13m 58s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

Starter Project

To help save some time, I’ve set up a starter project for us to begin from. Our AdonisJS API can be found within the backend directory and our Nuxt 3 application can be found in frontend.

Our AdonisJS API has Lucid already installed and configured and some stubbed methods we’ll be filling out in this lesson.

Our Nuxt 3 client has Nuxt UI installed, a form utility I like to use added, and a few pages stubbed so we’re not wasting time on HTML.

You can find the starter repository here, and follow along if you wish.

Configuring Our AdonisJS API

First, let’s get AdonisJS Auth installed.

npm i @adonisjs/auth
Copied!

Next, let’s configure it within our project, use the below selections to answer each configuration prompt.

node ace configure @adonisjs/auth

# › Select provider for finding your users → Lucid
# › Select which guard you need for authentication → Web
# › Enter model name to be used for authentication → User
# › Create migration for users table? → True
Copied!

You may be wondering why are we using “web” instead of “api” for our auth guard. Since our Nuxt 3 application will live on the web and be accessed via browsers we’ll have access to cookies. Meaning, we’re free to use sessions to manage our authentication, making “web” the optimal solution for our use case.

Registering Silent Auth Middleware

I’m a fan of utilizing the silent auth middleware to authenticate each request, so let’s go ahead and register it. This is one of the middleware added to our project when we configured the AdonisJS Auth package.

Server.middleware.register([
  () => import('@ioc:Adonis/Core/BodyParser'),
  () => import('App/Middleware/SilentAuth') // 👈
])
Copied!
  • start
  • kernel.ts

Installing Session Driver

Next, since this was started from the AdonisJS API template, we’ll need to install the session driver.

npm i @adonisjs/session
Copied!

Then, configure it.

node ace configure @adonisjs/session
Copied!

Lastly, add the env type to your env.ts file

// env.ts

SESSION_DRIVER: Env.schema.string()
Copied!

Enable CORS

Next, in order for our requests to succeed from our Nuxt 3 application, we’ll need to enable CORS. Since the domain will be shared, we don’t need to worry about anything else besides enabling it.

const corsConfig: CorsConfig = {
  enabled: false,
  enabled: true,

  // ...
}
Copied!
  • config
  • cors.ts

Migrating Your Database

Lastly, let’s go ahead and migrate our database. At this point, this should just be our users table getting added with the default columns.

node ace migration:run
Copied!

API Auth Routes

Since we’re using sessions for authentication, we can keep things simple here without dealing with tokens. All route handling is done within start/routes.ts for brevity.

Signing Up

First, let’s tackle signing up.

Route.post('/auth/sign-up', async ({ request, response }) => {
Route.post('/auth/sign-up', async ({ request, response, auth }) => {
  const data = await request.validate(AuthSignUpValidator)
  
  throw new NotImplementedException('POST /auth/sign-up has not been implemented')
  const user = await User.create(data)
  await auth.login(user)

  return response.json({ message: 'Thanks for joining!' })
})
Copied!
  • start
  • routes.ts

Nothing special here, we’re just validating the data, creating our user, and logging that new user in using the default “web” guard.

Signing In

Next, let’s complete signing in.

Route.post('/auth/sign-in', async ({ request, response }) => {
Route.post('/auth/sign-in', async ({ request, response, auth }) => {
  const data = await request.validate(AuthSignInValidator)
  const { uid, password, rememberMe } = await request.validate(AuthSignInValidator)
  
  throw new NotImplementedException('POST /auth/sign-in has not been implemented')
  try {
    await auth.attempt(uid, password, rememberMe)
  } catch (_error) {
    return response.status(400).json({ message: 'Email or password is incorrect' })
  }

  return response.json({ message: `Welcome back!` })
})
Copied!
  • start
  • routes.ts

Again, nothing special, we’re validating the data and logging in the user.

Auth Check & Signing Out

Let’s finish up our API endpoints by completing our auth check and sign-out.

Route.get('/auth', async () => {
Route.get('/auth', async ({ auth }) => {
  throw new NotImplementedException('GET /auth has not been implemented')
  return auth.user
})
Copied!
  • start
  • routes.ts

Here we’re simply returning auth.user, this is nullable so if a user isn’t authenticated we’ll get back null. Otherwise, we’ll get the user.

--Route.post('/auth/sign-out', async () => {
Route.post('/auth/sign-out', async ({ auth }) => {
  throw new NotImplementedException('POST /auth/sign-out has not been implemented')
  await auth.logout()

  return response.json({ message: 'You have been signed out' })
})
Copied!
  • start
  • routes.ts

Configuring Our Nuxt 3 Front End

The one thing we’ll need to configure within our Nuxt 3 application is an environment variable with our API url, since this will need to change between local and production environments.

Adding API Environment Variable

First, add a .env file at the root of our Nuxt 3 project with the following inside.

NUXT_PUBLIC_API=http://localhost:3333

Here, NUXT_PUBLIC is a prefix defined by Nuxt designating this environment variable is public and not a secret or confidential. The actual variable name here is just API.

Easily Accessing Our API Environment Variable

Next, let’s create a global accessor within our Nuxt project to simplify accessing our API environment variable.

Create a folder called plugins with a file inside it called env.ts and add the following inside.

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  return {
    provide: {
      api: (path: string) => config.public.api + path,
    }
  }
})
Copied!
  • plugins
  • env.ts

With this added, we’ll now be able to access our API variable anywhere in our Nuxt project with the following line:

const { $api } = useNuxtApp()

Signing Up

Since form submissions are initiated on the client side of Nuxt instead of the server, they’re rather straightforward. They’ll automatically pass along our session cookie so the server automatically knows who sent the request.

For brevity, we’ve already set up the forms for this project.

// pages/auth/sign-up.vue

<script setup>
  import useForm from '~/utilities/form'

  const toast = useToast()
  const { $api } = useNuxtApp()
  const form = useForm({
    email: '',
    password: '',
  }, toast)

  const signUp = async () => {
    await form.value.post($api('/auth/sign-up'))
    toast.add({ title: 'Thanks for joining!', timeout: 6000 })
    navigateTo({ path: '/' })
  }
</script>
Copied!

Here we’re utilizing our useForm helper. This behaves similarly to InertiaJS’s useForm helper if you’ve used it. All data within it is reactive, and it houses an API layer as well as our form’s data.

Within our signUp method, all we need to do is post out to our API’s /auth/sign-up endpoint. Once completed the response will send back our updated session cookie with our authenticated user attached. So, we’re good to forward along to the home page.

Logging In

Logging in will be the same process as signing up, just submit the form and our API will take care of authenticating our user and updating our session.

// pages/auth/sign-in.vue

<script setup>
  import useForm from '~/utilities/form'

  const toast = useToast()
  const { $api } = useNuxtApp()
  const form = useForm({
    uid: '',
    password: '',
    rememberMe: false
  }, toast)

  const signIn = async () => {
    await form.value.post($api('/auth/sign-in'))
    toast.add({ title: 'Welcome back!', timeout: 6000 })
    navigateTo({ path: '/' })
  }
</script>
Copied!

Fetching Our Logged In User

Now it’s time to fetch our authenticated user, putting our flow to the test. Within our home page, let’s update our script to the following.

Fetching Without Session

<script setup>
  import useForm from '~/utilities/form'

  const toast = useToast()
  const { $api } = useNuxtApp()

  const user = null
  const { data: user } = await useFetch($api('/auth'))

  const logoutForm = useForm({}, toast)
  const logout = async () => {
    await logoutForm.value.post($api('/auth/sign-out'))
    toast.add({ title: 'You have been logged out', timeout: 6000 })
    user.value = null
  }
</script>
Copied!
  • pages
  • index.vue

Now, I knowingly have this incorrect so that we can see why this doesn’t work. Our API call will go out to our /auth endpoint, however, the call won’t find our authenticated user for a couple of reasons.

  1. We’re not including credentials with our fetch call

  2. If this were to send on the server-side our session won’t be passed along at all.

So go ahead and give the above a test and see what happens for yourself. The page should load just fine, but it will appear as though the user is not logged in.

Fetching With Session

Now, let’s fix our above implementation. First, let’s fix the server-side call so that it’ll include our session.

For this, Nuxt provides a convenient helper that will pluck the cookies off the Nuxt Server’s request and proxy it to our API’s request to our AdonisJS server.

const headers = useRequestHeaders(['cookie'])
const { data: user } = await useFetch($api('/auth'), { headers })
Copied!

With this, if we refresh our page, we’ll start getting our user loaded in. However, if we were to traverse on the client side from page to page in our application, the user would not load in. This is because we’re not passing the credentials from the browser on the client side. To fix that, all we need to do is add credentials: 'include' to our fetch config.

const headers = useRequestHeaders(['cookie'])
const { data: user } = await useFetch($api('/auth'), { headers, credentials: 'include' })
Copied!

Now our user should get successfully fetched on both the Nuxt server and client.

That’s it! For any API calls where you need your user to be authenticated, just be sure to pass along the headers and credentials and your authentication will automatically apply, no worrying about tokens or expirations and your Nuxt app can be as oblivious to your authentication system as you desire.

From here, if you wish, you could add a Pinia store to store your user’s details, and a Nuxt middleware to check the authentication and populate the user on each request.

Join The Discussion! (4 Comments)

Please sign in or sign up for free to join in on the dicussion.

  1. Commented 7 months ago

    Hello!

    What about middleware?

    1

    Please sign in or sign up for free to reply

    1. Commented 6 months ago

      The same requirements mentioned above should apply for Nuxt middleware. If you're making any call to your AdonisJS API where you need to appear authenticated, just be sure to pass along the cookie headers and include credentials.

      You could also store your authentication state within a Pinia store and reference it directly inside any Nuxt middleware.

      0

      Please sign in or sign up for free to reply

  2. Commented 5 months ago

    got Unauthorized access when I use middleware auth

    1

    Please sign in or sign up for free to reply

    1. Commented 5 months ago

      That would mean that in the eyes of your AdonisJS application, you're not authenticated. Please double-check that you've properly implemented all the steps covered within the "Fetching Our Logged In User" section.

      Most notably, this block here, as this is the block that will pass along the needed headers for both client-sent and server-sent requests from Nuxt.

      const headers = useRequestHeaders(['cookie'])
      const { data: user } = await useFetch($api('/auth'), { headers, credentials: 'include' })
      Copied!
      0

      Please sign in or sign up for free to reply