Skip to content

Multi-tenant application: Auth cookies are duplicated when the access token gets refreshed, causing incessant calls to the token refresh endpoint. #30084

@jeromevvb

Description

@jeromevvb

Hi all,

I am building a multi-tenant platform with NextJS using Supabase.
The application for my customers is under app.localhost.com,
and each customer can create their blog. For example, blog.localhost.com, blog2.localhost.com

Naturally, I will set the domain for cookies under .localhost.com as the code below shows:

import {HOSTNAME, isDevelopment} from '@/lib/const'
import {Database} from '@/supabase/database.types'
import {createServerClient} from '@supabase/ssr'
import {cookies} from 'next/headers'

export const createClient = () => {
  const cookieStore = cookies()

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookieOptions: {
        domain: isDevelopment ? '.localhost.com' : HOSTNAME,
      },
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({name, value, options}) => {
              cookieStore.set(name, value, options)
            })
          } catch (error) {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}

This works pretty well; however, when the cookie is about to expire, supabase will request the endpoint token?grant_type=refresh to refresh the access token.

Describe the bug

When I set domain to my cookies, something happened: the expiring cookies are not being updated, and new cookies are being set. If I open my console, I will see four cookies:

Screenshot 2024-10-24 at 9 39 26 PM

Then, I will see infinite requests to the token?grant_type=refresh endpoint because the expiring cookies are still there. It will eventually fail, and I will get disconnected...

If I don't set any domain, this works well.
I can tell that this bug also happened in the production environment on my supabase paid instance.

Here's my middleware code for reference:

import {createServerClient} from '@supabase/ssr'
import {NextRequest, NextResponse} from 'next/server'
import {HOSTNAME, isDevelopment} from './lib/const'

export default async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({name, value, options}) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  await supabase.auth.getUser()

  const url = new URL(request.url)
  const hostname = request.headers.get('host')

  const searchParams = url.searchParams.toString()
  const path = `${url.pathname}${searchParams.length > 0 ? `?${searchParams}` : ''}`

  // rewrites for app pages
  if (hostname === `app.${HOSTNAME}`) {
    response = NextResponse.rewrite(new URL(`/app${path === '/' ? '' : path}`, request.url))
  }
  // rewrite root application to `/home` folder
  else if (hostname === HOSTNAME || hostname === `www.${HOSTNAME}`) {
    response = NextResponse.rewrite(new URL(`/home${path === '/' ? '' : path}`, request.url))
  }
  // rewrite everything else to `/[domain]/[slug] dynamic route
  else {
    response = NextResponse.rewrite(new URL(`/${hostname}${path}`, request.url))
  }

  return response
}

export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api routes
     * 2. /_next (Next.js internals)
     * 3. /_static (inside /public)
     * 4. all root files inside /public (e.g. /favicon.ico)
     */
    '/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)',
  ],
}

To Reproduce

  1. Add a domain name where cookies are being set
  2. In your supabase authentication settings, set the Access token (JWT) expiry time to 180seconds, for example
  3. Login with a user account
  4. After a minute, supabase will send a request to token?grant_type=refresh
  5. When opening your console, go to Applications > Cookies. Notice duplicate cookies
  6. Supabase will try to keep refreshing the cookies, and token?grant_type=refresh endpoint will kept being requested.

Expected behavior

Cookies should be updated and works seamlessly.
Thank you for your help!

  • OS: MacOS Sonoma 14.1

  • Browser (if applies) Arc browser

    "@supabase/ssr": "^0.5.1",
    "@supabase/supabase-js": "^2.45.5",

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions