supabase-js: v2.0.0 regression: Custom JWT (particularly in realtime Channels)
Bug report
Description
Using a custom JWT was a feature in v1 : https://supabase.com/docs/reference/javascript/auth-setauth
Per comment: https://github.com/supabase/gotrue-js/pull/340#issuecomment-1218065610 what we did was establish the custom header and it works for creating the client.
However, it’s not working with realtime channels. Upon inspection, the problem is that the internal initialization isn’t setting up RealtimeClient’s setAuth method: https://github.com/supabase/realtime#realtime-rls which in turn causes the web socket to not send the JWT on the messages and heartbeats…
The result is that channels fail the RLS.
A second problem is with setting up a new valid JWT when the previous one expired: there’s no way to do it. Not in the supabase client and much less in the realtime client.
To Reproduce
- Create a database with a restrictive policy based on a custom JWT
- Create a JWT with a 1 minute expiration time
- Create a client using the method explained in https://github.com/supabase/gotrue-js/pull/340#issuecomment-1218065610 4.a. Let 2 minutes go by and try to make any call to supabase using supabaseClient. Access is denied because the JWT has expired. Try to update the JWT -> there’s no way to do it. Would have to create a new client but that defeats the whole thing. 4.b. Create a channel subscription -> ‘fails’ by not providing access to rows to which the user has access acording to the policy)
Upon inspection of the websocket, as it was expected, the messages and heartbeats don’t include the custom JWT… Why whould they? We’ve only set the header /directly/ and that’s it…
Workaround
What we’ve done is changing from protected to public the supabase client’s class elements:
“headers” in SupabaseClient
“realtime” in GoTrueClient
and we’re calling
a) supabaseClient.realtime.setAuth(JWT)
b) supabase.auth.headers.Authorization = Bearer ${JWT}
;
with the customToken…
That keeps everything working…
Tried forking and creating an updateJWT method, but realized we’re not very familiar with the modularization philosophy of the project and was most likely being both overkill about it. Also, were falling short because an alternate method is quite likely required for the initialization, since using the headers option in the createClient doesn’t impact the realtime client.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 9
- Comments: 52 (14 by maintainers)
@kangmingtay Based on your comment, I made the realtime work with custom jwt. Here is how I handle it:
@w3b6x9 I’m migrating to Supabase from Firebase. I’m keeping firebase auth for now since Supabase doesn’t have anonymous auth yet. To summarize and clarify the issues based on the above, here’s what I currently do:
This method works up to the point where I need to refresh the user’s token, which I want to do whenever their firebase token gets refreshed. I can call my edge fn to verify the new firebase token and generate a new supabase token, but then as @LuisAngelVzz pointed out there isn’t a great way to set this new token in the existing client. For now I will use a workaround like was posted above.
I think there are two things needed to solve these problems:
signUpWithCustomToken
/signInWithCustomToken
option as suggested by @magicseth to create a real gotrue session based on the custom token.Are either of these things currently planned to be implemented? If so, when?
I hope this effectively summarizes the issue. IMO this is likely to be a problem faced by almost everyone attempting to migrate from Firebse so I hope it can get bumped up to a higher priority by the Supabase team.
What I was hoping for was a SignInWithToken(…) api that would take the JWT, create a session, and call the authstatechanged callbacks.
As is it feels daunting reaching in to protected fields in multiple files to achieve this.
@w3b6x9 We’ve just been testing this and want to highlight that the docs are slightly off. They say to pass the Supabase key into headers and the custom JWT into params, whereas it’s actually the other way around.
What worked for us is:
Whereas the docs say:
Note that the description of the Supabase anon key in the docs is also confusing:
“The
apikey
inheaders
must be either theanon
orservice_role
token that Supabase signed”Other than this needing to be the
apikey
inparams
notheaders
, it should probably also say “that Supabase provides” rather than “that Supabase signed”. This caused a mini goose chase when debugging.For other people (especially Clerk auth users) using a custom JWT, here are two patterns I’m using. Feel free to give feedback if you have a better way:
And just for completeness, my JWT code (for Clerk auth):
I did! See: https://supabase.com/docs/guides/realtime/postgres-changes#custom-tokens
And you can use the Inspector with a custom JWT here: https://realtime.supabase.com/inspector/new
Gonna close this one now!
hey @evelant, apologies for the late reply as the team was quite busy with launch week! just to clarify, i’m assuming that you’d want
updateToken(newToken: string)
to take care of setting the new custom token properly across the Supabase services like such:Ha…! Thanks…
I wonder if the issue of a customJWT refresh method is on the todo’s ? Not urgent… My workaround works without issues… (There may be something I may be missing, though…)
It’s all documented on https://github.com/supabase/gotrue-js/issues/701 along with patches to unprotect the applicable class properties:
which have to be updated in addition to calling setAuth again when the JWT expires…
cc @kangmingtay @w3b6x9
Annoyingly I’m still having problems with updating the client’s headers when I refresh my custom token. My code posted above worked fine against the local supabase instance in docker but is failing against my live staging instance. I verified that I am getting a correctly signed updated token but unfortunately postgrest and edge function requests are failing after I attempt to update the client.
@w3b6x9 @hf @kangmingtay Could you please provide an example of how you would update a custom token on an existing client instance?
I’m really struggling with this and it’s the last major piece of the puzzle necessary to transition my app to Supabase. We can’t go live with the switch to Supabase until token refresh is working reliably.
Hey folks, I wanted to address the current custom token situation with your hosted Supabase project’s Realtime.
The Realtime docs regarding custom tokens here are correct: https://supabase.com/docs/guides/realtime/extensions/postgres-changes#custom-tokens.
I’ve also created this Replit as an example of it working: https://replit.com/@w3b6x9/RealtimeCustomToken. You can scroll to the bottom for database setup if you would like to reproduce it.
Here’s how the Supabase provided
anon
token and your custom token are being used once passed in tosupabase-js
client.Providing this here for convenient reference:
The Supabase
anon
token that is provided to your Supabase project is checked in our Cloudflare API gateway to make sure that the project is valid and active. Then the gateway forwards the request, along with your custom token found underrealtime.params.apikey
in the options object, to our Realtime servers.Realtime servers will check your custom token and make sure it’s signed by your project’s JWT secret. If you’re listening to Postgres changes it will then insert this custom token’s claims into your project database’s
realtime.subscription
table.If you decide to reproduce my Replit example, you’ll see in the
realtime.subscription
table that the claims is that of the custom signed token.Like @evelant mentioned earlier (https://github.com/supabase/supabase-js/issues/553#issuecomment-1492242754) I have removed the
protected
flag so you can callsupabaseClient.realtime.setAuth(token)
when you want to pass Realtime a refreshed token withoutts
complaining.@yamen thanks, this is good feedback! will go and update the docs so it’s more obvious.
Ok, my bad, it was a silly mistake, essentially a typo. I had
authorization
where I should have hadAuthorization
. The workaround of manually issuing a new token and then setting it on the headers of the various client libraries seems to be working just fine now.@rdylina Frustratingly this workaround doesn’t seem to actually work. Once I’ve refreshed my token I still get invalid token failures.
JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 5)
. As far as I can tell from reading all the code in the libraries this workaround should work so I’m definitely missing something.@laktek @hf @soedirgo @J0 Could you provide some input on this issue please? Custom tokens + refresh is the final blocker for my team to finish migrating from Firebase to Supabase.
OK I seem to have gotten it working. Not sure why this is working, it’s not even close to what the docs suggest.
If I create a client like this – note I commented out any realtime config
then call
realtime.setAuth(key)
and now realtime gets the correct info from my custom token. RLS works andrealtime.subscription
shows the correct claims{"aud": "authenticated", "exp": 1680230955, "sub": "zsGXfLygdCt2OF4Vt0hGrodAuK7a", "role": "authenticated", "app_metadata": null, "user_metadata": null}
I’ll need to test this some more (specifically token refresh) but this may be a good enough workaround until the problems with custom tokens get fixed.
@rdylina After digging into it a little bit it seems the issue, at least for me, isn’t RLS specifically. Instead the problem is that realtime postgres change listeners are authenticated as
anon
, despite me setting the headers as suggested.Requests from postgres-js work fine with the token, they get the proper
authenticated
role andsub
. Realtime however does not appear to use the custom token and treats the connection asanon
so it probably fails most RLS rules preventing anonymous access.@w3b6x9 having realtime work with a custom token is a requirement for us to switch from firebase to supabase. We can’t migrate our auth yet so we have to continue using firebase auth which means we need to have custom tokens working or we can’t migrate to Supabase. Any further pointers or input on this issue would be greatly appreciated!
edit: looking at the
realtime.subscription
table I can see that theclaims
column is bogus for all my realtime listeners. It’s{"exp": 1983812996, "iss": "supabase-demo", "role": "anon"}
instead of the actual custom token I’m sending from the client.I also see this in the docker logs for the local realtime container when I try to start a subscription without giving
anon
privilegeAny news on this? It’s a regression… Any way to get the devs attention?