supabase / auth-js

An isomorphic Javascript library for Supabase Auth.
MIT License
318 stars 152 forks source link

supabase-js@2.42.5 breaks client auth with edge functions #881

Closed b-snel closed 2 months ago

b-snel commented 2 months ago

Bug report

Describe the bug

When I get the authorization header from the client in an edge function I'm unable to make a supabase client with the newest version of supabase-js (using deno edge functions). The behavior doesn't happen on 2.42.4 and below. I have rolled back but I wanted to bring this to your attention.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Go to '…'
  2. Click on '…'
  3. Scroll down to '…'
  4. See error

Expected behavior

Able to get user auth details. See code snippet at bottom of how I typically do that.

Screenshots

If applicable, add screenshots to help explain your problem.

System information

Additional context

`import { corsHeaders } from '../_shared/cors.ts' import { createClient } from "supabase-js" import { createSupabaseClient } from '../_shared/supabaseClient.ts'

Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) }

const authHeader = req.headers.get('Authorization')! const supabaseClient = createSupabaseClient(authHeader)

// Get the session or user object const { data } = await supabaseClient.auth.getUser() const user = data.user

if (!user) { throw new Error('User is null'); }

}`

and here is the definition for my client function: `import { createClient, SupabaseClient } from "supabase-js"

export function createSupabaseClient(authHeader: string): SupabaseClient { return createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', { global: { headers: { Authorization: authHeader } } } ); }`

darrendawson commented 2 months ago

I ran into this today as well when I deployed a new version of an existing edge function and immediately started getting Auth errors.

AuthSessionMissingError: Auth session missing!
    at https://esm.sh/v135/@supabase/auth-js@2.63.1/deno/auth-js.mjs:2:29428
    at _._useSession (https://esm.sh/v135/@supabase/auth-js@2.63.1/deno/auth-js.mjs:2:27511)
    at eventLoopTick (ext:core/01_core.js:64:7)
    at async _._getUser (https://esm.sh/v135/@supabase/auth-js@2.63.1/deno/auth-js.mjs:2:29101)
    at async https://esm.sh/v135/@supabase/auth-js@2.63.1/deno/auth-js.mjs:2:28964
    at async https://esm.sh/v135/@supabase/auth-js@2.63.1/deno/auth-js.mjs:2:26791 {
  __isAuthError: true,
  name: "AuthSessionMissingError",
  status: 400,
  code: undefined
}
zguo123 commented 2 months ago

Looks like it's affecting me as well.

AuthSessionMissingError: Auth session missing!
    at https://esm.sh/v135/@supabase/auth-js@2.63.1/esnext/auth-js.mjs:2:29430
    at _._useSession (https://esm.sh/v135/@supabase/auth-js@2.63.1/esnext/auth-js.mjs:2:27513)
    at eventLoopTick (ext:core/01_core.js:64:7)
    at async _._getUser (https://esm.sh/v135/@supabase/auth-js@2.63.1/esnext/auth-js.mjs:2:29103)
    at async https://esm.sh/v135/@supabase/auth-js@2.63.1/esnext/auth-js.mjs:2:28966 {
  __isAuthError: true,
  name: "AuthSessionMissingError",
  status: 400,
  code: undefined
}
CookedApps commented 2 months ago

Wasted 4 hours debugging this... 🥲

zguo123 commented 2 months ago

Wasted 4 hours debugging this... 🥲

LMAO, you were not alone. I restarted the database, and I reset my jwt tokens lmao.

andymitchell commented 2 months ago

+1

Observations:

Screenshot 2024-04-19 at 19 50 35
christophemenager commented 2 months ago

I confirm I have the same issue and rolling back to "supabase-js": "https://esm.sh/@supabase/supabase-js@2.42.4", in my import_map.json fixes the issue

Seems related to

CleanShot 2024-04-19 at 22 16 10@2x

zguo123 commented 2 months ago

Here's the solution:

via Discord:

No You have to strip the Bearer part out You just want a jwt. supabaseClient.auth.getUser(authHeader.split(' ')[1]); Another user just posted this. Not verified.

j4w8n commented 2 months ago

Here is my observation:

PR https://github.com/supabase/auth-js/pull/876 adds a check for data.session?.access_token inside of getUser(). If it doesn't exist, then processing stops and returns null - which is what devs are now seeing in edge functions, because there is no Supabase session at this point.

Previously, without the new PR code, it would have just made a fetch request to the auth backend using _request. Notice the headers and jwt properties.

/* https://github.com/supabase/auth-js/blob/v2.63.1/src/GoTrueClient.ts#L1182-L1185 */
return await _request(this.fetch, 'GET', `${this.url}/user`, {
  headers: this.headers,
  jwt: data.session?.access_token ?? undefined,
  xform: _userResponse,
})

And if you look at _request, it's taking the jwt value, if it exists, and sets the Authorization header - which means the auth backend looks at that header to grab the JWT, lookup the user, and return a user response. And even if jwt is undefined, it still passes any Supabase client headers. So by setting the Authorization header in the edge function's Supabase client, we were bypassing the need to pass-in the JWT to getUser() in order for this to work without a session.

/* https://github.com/supabase/auth-js/blob/v2.63.1/src/lib/fetch.ts#L127-L143 */
export async function _request(
  fetcher: Fetch,
  method: RequestMethodType,
  url: string,
  options?: GotrueRequestOptions
) {
  const headers = {
    ...options?.headers,
  }

  if (!headers[API_VERSION_HEADER_NAME]) {
    headers[API_VERSION_HEADER_NAME] = API_VERSIONS['2024-01-01'].name
  }

  if (options?.jwt) {
    headers['Authorization'] = `Bearer ${options.jwt}`
  }
...
  const data = await _handleRequest(
    fetcher,
    method,
    url + queryString,
    {
      headers,
      noResolveJson: options?.noResolveJson,
    },
    {},
    options?.body
  )
ffaubert commented 2 months ago

Seems like a pretty major change for such a minor version bump. This broke my app too.

jareddr commented 2 months ago

+1 This broke my app too. I was deploying new edge functions in dev today and thought I was having the worst coding day ever for an hour or two. Because my edge functions don't have a pinned supabase library version the new code was getting picked up whenever I would deploy a new version of an edge function.

I'm sure many people like me have used the supabase edge function auth docs as a starting point for how to get authenticated user data within an edge function. This suggests that you can leave the getUser() function empty and indeed it has been working that way for months prior.

I ended up having to go through every edge function and patch it like this

const supabaseClient = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
);
const JWT = req.headers.get('Authorization')?.split(' ')[1] || '';
const userData = await supabaseClient.auth.getUser(JWT);

I'm confused why the supabase client needs the same JWT twice. I don't know all of the intended uses of the getUser function, but surely there should be some convenience function within the supabase client that will grab the user data matching the authorization header passed in when creating the client.

I think my take away here is to not do this in my edge function code

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

and instead do this

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.42.4';

and then only upgrading when I explicitly intend to move to a new version.

GaryAustin1 commented 2 months ago

I'm going to move this to the auth repository as it seems to be the underlying auth-js release is the issue.

Hopefully the right move...

GaryAustin1 commented 2 months ago

@kangmingtay Does the supabase-js team know about this fix? Users are still running into the issue on Edge functions and your fix here has not been merged with supabase-js yet, or released.

zguo123 commented 2 months ago

@kangmingtay Does the supabase-js team know about this fix? Users are still running into the issue on Edge functions and your fix here has not been merged with supabase-js yet, or released.

You can use any version below 2.42.4.

Eg:

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.42.4';
GaryAustin1 commented 2 months ago

Sure and that's what I'm telling users that come to discord, or github and I see them. Or just use getUser(jwt) with the jwt from the header. BUT users are hitting this and not knowing what the issue is, so wasting alot of time on their part till they find the solution.

jareddr commented 2 months ago

Just to reinforce what GaryAustin1 is saying...

When I wrote in to support with this issue I got back:

Thank you for contacting Supabase support. This works and I have it in a few of my edge functions.

Have you tried logging the headers to debug it?

In reference to calling auth.getUser() with no JWT. It seems like the organization doesn't understand the existence or magnitude of the problem.

It's a big breaking issue for anyone who copied code form the JavaScript edge function auth docs. It's insidious in that it only shows up when you deploy a new version of your edge function and only if you are using the import line from the docs which just references @supabase/supabase-js@2.

Then you spend a lot of time trying to figure out how you completely broke your edge function with whatever change you made.