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:
- Setting up Supabase Auth with Nuxt 3
- Logging in and out with Github
- Protecting Routes
- 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 theuseSupabaseUser
composable. This gives us a plain JS object instead of a reactiveref
. - 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.