Auth in Nuxt 3: Protecting Server Routes (4 of 4)

In this final article, we will fix our security by locking down our server routes as well.

Michael Thiessen
Nuxt 3

Mastering Nuxt 3 course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view the Nuxt 3 course

It’s not great to leave our API endpoints unprotected, even if all of our routes are secure.

Thankfully, protecting our server routes doesn’t require a lot of code from us.

In this series, we’re covering how to use Supabase with Nuxt 3 to add auth to our apps:

  1. Setting up Supabase Auth with Nuxt 3
  2. Logging in and out with Github
  3. Protecting Routes
  4. Protecting Server Routes 👈 we’re here

In this final article, we’ll fix our security by locking down our server routes as well.

Server Middleware

These operate in a similar way to route middleware, but they are run on every single request. This is like how Express middleware work.

This is useful for adding or checking headers, or extending the request object with some extra data that can be used by your event handlers.

Protecting Server Routes

We’ll borrow the same pattern from our route middleware. This is what we wrote in the previous article:

export default defineNuxtRouteMiddleware((to, from) => {
  const user = useSupabaseUser();

  if (!user.value) {
    return navigateTo('/login');    
    }
});

We’ll create our server middleware file at ~/server/middleware/auth.js:

import { serverSupabaseUser } from '#supabase/server';

export default defineEventHandler(async (event) => {
  const user = await serverSupabaseUser(event);

    if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized',
    });
  }
});

There are a couple differences here:

  • We have to use serverSupabaseUser instead of the useSupabaseUser composable. This gives us a plain JS object instead of a reactive ref.
  • Instead of redirecting we simply throw an error using createError. Redirecting to a login page doesn’t really make sense for an API endpoint.

But this will require the user to be logged in for all of our API endpoints.

Protect Route Method

We can refactor this into a simple utility method so that we can apply this only to the routes we want.

First, we’ll move the file from ~/server/middleware/auth.js to ~/server/protectRoute.js. Then, we’ll refactor it into a method:

// If the user does not exist on the request, throw a 401 error
export default async (event) => {
  const user = await serverSupabaseUser(event);

  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized',
    });
  }
};

Now we can control when this protectRoute method is run, and only run it on event handlers that need to be protected. It’s as simple as placing it at the top of our event handler:

import protectRoute from '~/server/protectRoute`;

export default defineEventHandler(async (event) => {
  await protectRoute(event);

  // ...
});

Or placing it inside of some conditional logic if we want:

import protectRoute from '~/server/protectRoute`;

export default defineEventHandler(async (event) => {
  // Allow the first chapter to be viewed by anyone
    if (event.context.params.chapterSlug !== '1-chapter-1') {
      await protectRoute(event);
  }

  // ...
});

Dealing With Cookies

We run into an issue if we use a protected API route during SSR:

const { data, error } = await useFetch(protectedUrl);

Supabase maintains the users session by using a cookie that is passed back and forth between the server and client. By default, the SSR context doesn’t have access to this cookie. When the server tries to fetch from the protected API, the API doesn’t know we’re logged in so the request is blocked.

We can solve this issue by always passing along the cookie using the useRequestHeaders composable and modifying how we use useFetch or useAsyncData:

const { data, error } = await useFetch(protectedUrl, {
  headers: useRequestHeaders(['cookie']),
});

Now, during SSR, useRequestHeaders will pass the cookie along with the request, so we can successfully access the protected endpoint.

Wrapping Up

Through this series we’ve seen how to set up Supabase and Github to provide auth, and how to log in and out of our application.

The previous article showed how to lock down routes using route middleware, and finally, this article showed how to protect API endpoints.

Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on