Supabase Auth with Next.js 12 middleware

Vladimir Klimontovich

CEO and Co-Founder
November 15th, 2021

Introduction

At Jitsu, we are big fans of Next.js. This website (jitsu.com) is built with it, and recently used Next.js to re-build Jitsu.Cloud billing server.

Few, we started another internal project and decided to experiment with Supabase and Next.js middleware. Here’s what we learned in the process

TL;DR - skip to What we’re going to build if you're familiar with Supabase and Next.js

What is Next.js

Next.js is an open-source a web-development framework built on React. Unlike with React, you can write server-side code with Next.js too. In fact, every Next.js application is mix of backend and frontend. Next.js is maintained by Vercel. They make money out of paid Next.js hosting services.

What is Next.js middleware?

Vercel team introduced middleware in recent v12 release. Middleware is just a file (_middleware.js or middleware.ts) that you can place anywhere in pages/ folder of your Next.js project. All requests to pages in this folder (and subfolders) will go through middleware.ts). Middleware file can decided if request should go through, alter request and etc/ Think of middleware as filter for particular sub-tree of your pages.

pages/ folder/ middleware.ts page.tsx

One of great use-cases for middleware is authorization. Example (middleware.ts):

export async function middleware(req: NextRequest, ev: NextFetchEvent) {
  if (!isValidUser(req)) {
    return NextResponse.redirect(`/login`)
  } else {
    //allow request to go through
    return NextResponse.next()
  }
}

What is Supabase?

Supabase is an open-source Firebase alternative. It allows web developers to "outsource" backend logic - such as authorization, structured data storage (SQL) and file storage.

What we’re going to build

The complete example can be found at @jitsucom/supabase-nextjs-middleware GitHub repo

We’re going to build a simple Next.js app which consist of index page (index.ts) and few pages in app/ folder. Index page will implement login/logout form and will be accessible for all users. We're going use Google as login provider, but you can configure any supported provider

app/ pages will be hidden from unauthorized users. All unauthorized users will be redirected to login (index page)

Login page

(See full source code here)

This page is fairly simple and standard. The authorization happens on a client side.

The only thing that is not standard is snippet

await fetch("/api/auth/set", {
  method: "POST",
  headers: new Headers({ "Content-Type": "application/json" }),
  credentials: "same-origin",
  body: JSON.stringify({ event, session }),
})

After successful login, we're calling /api/auth/set API that sets a server cookie sb:token. The cookie can be read by middleware.ts later.

pages/api/auth/set.ts implements that API:

export default async function handler(req, res) {
    await supabase.auth.api.setAuthCookie(req, res)
}

And we need to remove cookie when user logs out. We need to call API too:

await fetch("/api/auth/remove", { method: "GET", credentials: "same-origin" })

See implementation in pages/api/auth/remove.ts. Supabase haven’t implemented removeAuthCookie() yet, so we have to do it manually

export default async function handler(req, res) {
  res.setHeader('Set-Cookie', 'sb:token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT');
  res.send({});
}

Middleware

Here’s middleware.ts:

export async function middleware(req: NextRequest, ev: NextFetchEvent) {
  let authResult = await getUser(req)
  if (authResult.error) {
    console.log("Authorization error, redirecting to login page", authResult.error)
    return NextResponse.redirect(`/?ret=${encodeURIComponent(req.nextUrl.pathname)}`)
  } else if (!authResult.user) {
    console.log("No auth user, redirecting")
    return NextResponse.redirect(`/?ret=${encodeURIComponent(req.nextUrl.pathname)}`)
  } else {
    console.log("User is found", authResult.user)
    return NextResponse.next()
  }
}

All interesting staff happens in getUser method. Supabase has an API - supabase.auth.api.getUserByCookie, but unfortunately it doesn't work for Next.js. Good thing is that we can implement verify the auth manually by calling /auth/v1/user Supabase end-point

let token = req.cookies["sb:token"]
if (!token) {
  return {
    user: null,
    data: null,
    error: "There is no supabase token in request cookies",
  }
}
let authRequestResult = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/user`, {
  headers: {
    Authorization: `Bearer ${token}`,
    APIKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || "",
  },
})

let result = await authRequestResult.json()
console.log("Supabase auth result", result)
if (authRequestResult.status != 200) {
  return {
    user: null,
    data: null,
    error: `Supabase auth returned ${authRequestResult.status}. See logs for details`,
  }
} else if (result.aud === "authenticated") {
  return {
    user: result,
    data: result,
    error: null,
  }
}

Accessing user from SSR environment ( getServerSideProps)

Now, all authorized users will be able to access all pages inside app/. In the browser, Supabase user can be obtained through supabase.auth.user() call (don’t forget to wrap it in useEffect(), otherwise the code will be executed as SSR).

But what if we want to access current user user information during SSR — inside getServerSideProps? Here’s what we can do:

Supabase has an API for that — supabase.auth.api.getUser(). But this function queries Supabase HTTP API to validate the token. But it's redundant since the token has been already validated in middleware.ts call. Luckily, Supabase token is a JWT token, meaning that all user information is encoded inside it. The token also contains the signature that should be verified. But verification has been done in middleware.ts so we can just decode the token and skip verification.

See pages/app/hidden-page.ts

const Hidden: NextPage<{user: any}> = (props) => {
  return <h1>Do not tell anybody about this page, {props.user.email}</h1>
}

export async function getServerSideProps(context) {
  let supabaseToken = context.req.cookies['sb:token']
  if (!supabaseToken) {
    throw new Error("It should not happen! Since this page is guarded by _middlware.ts the presense of supabase token cookie (sb:token) should be already checked")
  }
  return {
    props: {
      //we do not need to verify JWT signature since it has been already done in _middlware.ts
      user: jwt.decode(supabaseToken),
    },
  }
}

(Don’t forget to yarn add jsonwebtoken or npm install jsonwebtoken!)

The complete example

The complete application example with all deployment and configuration example can be found [on our GitHub] (https://github.com/jitsucom/supabase-nextjs-middleware/blob/main/pages/app/hidden-page.tsx).

About Jitsu

Jitsu is an open-source data integration platform offering features like pulling data from APIs, streaming event-base data to DBs, multiplexing and many others.
© Jitsu Labs, Inc

2261 Market Street #4109
San Francisco, CA 94114