supabase / supabase-js

An isomorphic Javascript client for Supabase. Query your Supabase database, subscribe to realtime events, upload and download files, browse typescript examples, invoke postgres functions via rpc, invoke supabase edge functions, query pgvector.
https://supabase.com
MIT License
2.86k stars 220 forks source link

AuthApiError: invalid claim: missing sub claim #641

Closed alexvcasillas closed 1 year ago

alexvcasillas commented 1 year ago

Bug report

Describe the bug

Credentials are malformed are not being stored properly and therefore the client is not able to lately resolve the session

image

After calling supabase.auth.signInWithPassword({email, password}) this strangely formatted token is generated

'["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjY4NjAyNTM1LCJzdWIiOiJlYThiMjkxYS0wZTVkLTQ0YmEtYmViYi0zOGViYTQ1Y2UyOTgiLCJlbWFpbCI6ImFsZXh2Y2FzaWxsYXNAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwic2Vzc2lvbl9pZCI6ImFkN2FmYzRiLTZmYzQtNGQxOC04OWJiLTRiYmM1YmNmMzUxMyJ9.LK3i6Dyh-dZX-yk4NvgEdxYwmxsoOnAk1vfK-0Nmoe4","ktuUPxuJF-PHPhRpWfvUfQ",null,null]'

And later on, doing await supabase.auth.setSession(token); throws the following error:

{
  error: AuthApiError: invalid claim: missing sub claim
      at /home/alexvcasillas/otpfy/otpfy-v2/node_modules/@supabase/gotrue-js/dist/main/lib/fetch.js:41:20
      at processTicksAndRejections (node:internal/process/task_queues:96:5) {
    __isAuthError: true,
    status: 401
  }
}

This is the versions that I'm currently using:

"@supabase/auth-helpers-nextjs": "^0.5.2",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/supabase-js": "^2.1.0",

This worked before with versions:

 "@supabase/auth-helpers-nextjs": "^0.4.0-next.4",
"@supabase/auth-helpers-react": "^0.3.0-next.4",
"@supabase/supabase-js": "^2.0.0",

And I have my product live without issues https://otpfy.com, but after upgrading everything to latest, it stopped working as expected 🤔

To Reproduce

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

I'm using next for everything related

Sign in a user server-side

import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextApiRequest, NextApiResponse } from "next";
import { Database } from "../../types/database";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { email, password } = req.body;

  if (!email || !password) {
    res.status(400).send({ data: null });
    return;
  }

  const supabaseServerClient = createServerSupabaseClient<Database>({
    req,
    res,
  });

  const { data, error } = await supabaseServerClient.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    res.status(500).send({ data: null, error: error.message });
    return;
  }

  res.status(200).send({ data: data });
}

Try to see if the user is authenticated after server-side:

import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextApiRequest, NextApiResponse } from "next";
import { fetchUser } from "../../services/user";
import { Database } from "../../types/database";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const supabaseServerClient = createServerSupabaseClient<Database>({
    req,
    res,
  });

  const supabaseAuthToken = req.headers["supabase-auth-token"];

  if (supabaseAuthToken === "undefined") {
    res.status(401).send({ data: null });
    return;
  }

  const token = JSON.parse(supabaseAuthToken as string) as SupabaseAuthToken;

  await supabaseServerClient.auth.setSession(token);

  const {
    data: { user },
    error,
  } = await supabaseServerClient.auth.getUser(token.access_token);

  if (error) {
    res.status(401).send({ data: null });
    return;
  }

  if (user) {
    const userData = await fetchUser(user.id, supabaseServerClient);

    res.status(200).send({ data: userData ?? null });
    return;
  }

  res.status(500).send({ data: null });
}

Expected behavior

I should be able to sign up a user and work fine as expected

System information

j4w8n commented 1 year ago

I can't reproduce the malformed credential. I'm using OAuth instead of email/pass; but I'd assume a JWT is created with the same code, no matter which is used.

As for setSession(), we're now required to pass in a "session". But not a whole session, just the access_token and refresh_token. The docs have not been fully updated to reflect this, but the Parameters section is correct.

const { data, error } = await client.auth.setSession({ access_token: 'value', refresh_token: 'value' })
alexvcasillas commented 1 year ago

After following this example from the auth-helpers repository I managed to have all these features fully working and working like I charm I have to say.

Having upgraded to Next 13 seemed to be the root cause of this issue as now I can see that the supabase-auth-token is looking the same way as the one that I pointed out to be "malformed" because it didn't look like in the previous version.

I think it's safe to close this bug report as it relates to using supabase-js the same way as we did for next<13, which now is different and I've figured it out thanks to the given example. Hope others can find this example helpful too :)

Spiralis commented 1 year ago

I am having the same issue. But I am not in NextJS-land.

I am working on a server-side Solid Start solution, using cookies. Having said that the reproducing sample is just about identical.

The only difference is that I am getting the access_token and refresh_token from a cookie.

The weird thing is that the setSession call completes fine (I am passing an object with the access and header tokens). It even returns the session-data and the error property is indeed null.

The error comes from the getUser() call. And it is exactly the same as described in this issue.

A few days ago when I started working on the code, it was working too (AFAICT). I first then started to see this a few times and now it is there every time.

Is it necessary to even call getUser? The samples all show the setSession call without using the returned data, which includes the user. Can't I just use the user returned?

In this app I am not exposing the supabase-client at all in the browser. All the code is run server-side. I am creating a secure cookie that contains the access and refresh tokens (encrypted). So, every request from the client to the server will pass that cookie and I will then be able to authenticate the user (via setSession).

Should I then update the refresh-token in my cookie after the call, if it has changed? As I believe that it may have been rotated - and I guess it should not be reused.

Spiralis commented 1 year ago

Although, I haven't tried to actually do anything after the setSession call yet. That might fail just as badly as the getUser call of course... I have just been passing the user-data back to tell the browser-client that the user is logged in. Got to go now, but I guess that is something that I will need to test...

Spiralis commented 1 year ago

Not sure if this is a perfect solution, but I made some option-changes to the call to createClient, which seems to have fixed the issue for me.

const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    autoRefreshToken: false, // All my Supabase access is from server, so no need to refresh the token
    detectSessionInUrl: false, // We are not using OAuth, so we don't need this. Also, we are manually "detecting" the session in the server-side code
    persistSession: false, // All our access is from server, so no need to persist the session to browser's local storage
  },

It seems that after I added persistSession: false the getUser call no longer resulted in an error.

j4w8n commented 1 year ago

Not sure if this is a perfect solution, but I made some option-changes to the call to createClient, which seems to have fixed the issue for me.

const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    autoRefreshToken: false, // All my Supabase access is from server, so no need to refresh the token
    detectSessionInUrl: false, // We are not using OAuth, so we don't need this. Also, we are manually "detecting" the session in the server-side code
    persistSession: false, // All our access is from server, so no need to persist the session to browser's local storage
  },

It seems that after I added persistSession: false the getUser call no longer resulted in an error.

Yeah, there's no good docs on this. For getUser() to work server-side, persistSession needs to be false.

soedirgo commented 1 year ago

@Spiralis @j4w8n thanks for bringing this up folks, I'm passing this as a feedback to the Auth team :)

j4w8n commented 1 year ago

No problem @soedirgo. What I said is only true if you don't pass an access token into getUser. Because in that case, it calls getSession; which is where things really breakdown.

Gary opened an issue about it https://github.com/supabase/gotrue-js/issues/539

There are also issues with using setSession on the server side without setting persistSession to false. Not because of that method's direct code, but because it calls _saveSession and it's code causes issues.

Spiralis commented 1 year ago

I have also added passing the access_code (since I have it from setSession), as I understand that this reduces the work that getUser need to do.

j4w8n commented 1 year ago

Yep, that's a good way to do it on the server side.

jeremyisatrecharm commented 1 year ago

I was getting this error because I have an Express backend and want to attach the user object in Express middleware. I am passing the accessToken / refreshToken via the serverSideRendering docs.

In the middlware, I was calling

const supabase = createClient<Database>(
      SUPABASE_URL ?? "",
      SUPABASE_ANON_KEY ?? "",
    );
supabase.auth.setSession(/*tokens*/)
// ...
supabse.auth.getUser() // returned the invalid claim error

Adding this flag (as referenced above) seems to have fixed it:

      // Without this flag, we get "invalid subclaim" error on supabase.auth.getUser
const supabase = createClient<Database>(
      SUPABASE_URL ?? "",
      SUPABASE_ANON_KEY ?? "",
      {
        auth: {
          persistSession: false,
        },
      }
    );
NorweskiDrwal commented 1 year ago

I am experiecing this issue too.

I have an nx.dev monorepo with an SPA in React sporting Vite and an express app for backend stuff (both will go inside Tauri app).

Before I landed on the invalid claim: missing sub claim problem I have been debugging a different issue also related to how I was using Supabase. I was getting util.inherits is not a function, because I was trying to use one client for both express and react app (it is a monorepo, why duplicate configs). Anyway, one guy was having an issue with the util.inherits and the underlaying problem was that he was using jsonwebtoken that doesn't want to work with Vite because some nodejs APIs are not polyfilled. I split my code into two separate clients, which solved the util.inherits is not a function bug, but it gave me the invalid claim: missing sub claim one.

Could it be that the underlying issue is in Supabasejs not communicating properly with server-based build frameworks? This library is using jsonwebtoken. The "bug progression" on my end could indicate that jsonwebtoken is missing context, since it is not polyfilled anywhere. This problem also exists for browser's storage APIs and I bet jsonwebtoken is used here to en/decode data.

Since Supabasejs is isomorphic the solution could be in polyfilling jsonwebtoken's missing node APIs or replacing jwt with eg. jose that seems to be supported across environemnts.

I have no idea if any of this applies to invalid claim: missing sub claim, but I am going to debug it some more to narrow down the problem.

TranquilMarmot commented 11 months ago

I ran into this issue tonight, getting invalid claim: missing sub claim, and ended up on this issue so I'll add my solution here for intrepid explorers 😄

I'm doing something similar to what was mentioned above with a client that logs in via Supabase, then sends the access/refresh tokens to a separate server.

The problem for me was that client.auth.setSession returns a Promise! I was just missing an await...

const supabaseUserClient = createClient(supabaseUrl, supabasePublicKey, {
  auth: {
    autoRefreshToken: false,
    persistSession: false,
  },
});

// you have to `await` this promise for the session to actually be set!
await supabaseUserClient.auth.setSession({
  access_token: accessToken,
  refresh_token: refreshToken,
});

return supabaseUserClient;
clemwo commented 7 months ago

I'm having that issue using supabase with expo. When I set persistSession: false authentication works but I definitely want to persist my session. Any idea if there is another way to fix?

j4w8n commented 7 months ago

I'm having that issue using supabase with expo. When I set persistSession: false authentication works but I definitely want to persist my session. Any idea if there is another way to fix?

I'd recommend opening your own issue. This one is pretty stale. You can also ask on the Discord server.

khuongduy354 commented 4 months ago

I also encounter this problem, set persistSession to false not works for me, and also this answer link is dead, so this problem is not resolved for me. Is this isssue answered somewhere or is it not fix yet?

clemwo commented 4 months ago

I also encounter this problem, set persistSession to false not works for me, and also this answer link is dead, so this problem is not resolved for me. Is this isssue answered somewhere or is it not fix yet?

I'm not 100% sure I understand your problem but have you tried the solution from my comment in another issue:

https://github.com/supabase/supabase-js/issues/786#issuecomment-1792692206

devodii commented 1 month ago

Too bad to see that this error still exists. Especially when making a POST request inside a Next js /app dir.

How do we get around this? 🤔

yahorbarkouski commented 3 weeks ago

Any workarounds? Using supabase on both server and client log-outs users from time to time, really annoying :( @awalias can we please reopen this issue since the error still occurs?

timobehrens commented 3 days ago

Same issue here on Safari, working fine on Chrome.