How to use RLS when using better-auth

Hey everyone! I have a next.js app and I use supabase + better-auth for authentication. I would like to use the supabase browser client and for that I will need to setup policies (RLS) but I'm not sure how can I get the user id in the database queries. the auth.uid() works only when using supabase auth feature. I already tried to pass custom JWT in the headers and created test RPC function that returns the user id from the jwt but it doesn't recognize it. is there a way to do it?
45 Replies
garyaustin
garyaustin3h ago
The sub claim in the JWT is what is pulled from auth.uid(). As long as the JWT is valid for Supabase things like role and claim are passed on to the auth functions and RLS. https://supabase.com/docs/guides/auth/jwts https://supabase.com/docs/guides/auth/jwt-fields
Jacob
JacobOP2h ago
@garyaustin just like you said it worked with the default database functions to get user id / role when used similar format that it expects! However better auth user id format is different from supabase and it gives me this error
"error": "invalid input syntax for type uuid: \"ucxO2OE75ptv0BPbUhPPIYV7waY0ojh6\""
"error": "invalid input syntax for type uuid: \"ucxO2OE75ptv0BPbUhPPIYV7waY0ojh6\""
If I hardcode dummy user id it works fine
const jwtPayload = {
iss: 'supabase',
aud: 'authenticated',
exp: Math.floor(Date.now() / 1000) + 60 * 15, // 15min expiry
iat: Math.floor(Date.now() / 1000),
sub: '550e8400-e29b-41d4-a716-446655440000',
role: 'authenticated',
aal: 'aal1',
session_id: session.session.id, // if available, else generate UUID
email: session.user.email,
phone: '',
is_anonymous: false
};
const jwtPayload = {
iss: 'supabase',
aud: 'authenticated',
exp: Math.floor(Date.now() / 1000) + 60 * 15, // 15min expiry
iat: Math.floor(Date.now() / 1000),
sub: '550e8400-e29b-41d4-a716-446655440000',
role: 'authenticated',
aal: 'aal1',
session_id: session.session.id, // if available, else generate UUID
email: session.user.email,
phone: '',
is_anonymous: false
};
is there a way to get stuff from it without the default functions of supabase? my only goal is to make RLS usable with better auth
garyaustin
garyaustin2h ago
If your sub is not a UUID type from you auth provider then you can't use auth.uid(). You would need to decode your own claim with auth.jwt()->>'sub' or auth.jwt()->>'id'.
Jacob
JacobOP2h ago
I'm also worried that I need to match the format of supabase in the jwtPayload
garyaustin
garyaustin2h ago
auth.jwt will get any claim in a valid JWT signed by the supabase secret. You must provide your JWT in the Authorization header.
Jacob
JacobOP2h ago
If your sub is not a UUID type from you auth provider then you can't use auth.uid(). You would need to decode your own claim with auth.jwt()->>'sub' or auth.jwt()->>'id'.
I will need to validate the jwt on my own? like creating similar function like the default one of supabase?
You must provide your JWT in the Authorization header.
It should be in the header ofc
garyaustin
garyaustin2h ago
No you need to sign it with the JWT secret. Then Supabase will validate it.
Jacob
JacobOP2h ago
I configured it with createClient<Database>(supabaseUrl, supabaseKey, { accessToken: getAccessToken });
garyaustin
garyaustin2h ago
Yes. That function sets the authorization header. So you have to provide minimum claims plus your own and then sign it with the Supabase JWT secret.
Jacob
JacobOP2h ago
Is there a way to manually validate the jwt payload? on the supabase side, as db function
garyaustin
garyaustin2h ago
Not on the Supabase server side. It is automatic with signing with the JWT secret.
Jacob
JacobOP2h ago
So you have to provide minimum claims plus your own and then sign it with the Supabase JWT secret.
that's what I did, but supabase refuse to claim it because of the format of the user-id of better-auth
garyaustin
garyaustin2h ago
Yes. auth.uid() won't work if sub is not a UUID. So either don't use auth.uid() or put your claim in a different claim. I don't remember if sub as a UUID is required to get into postgREST but I don't think so. Just auth.uid() won't work.
Jacob
JacobOP2h ago
Not using auth.uid() sounds better as I don't use the auth feature of supabase but I'm not sure then how I will be able to validate policies
garyaustin
garyaustin2h ago
Then you use auth.jwt()->>'sub' Where sub is a string. Compare that to a string id column in the tables.
Jacob
JacobOP2h ago
As far as I understand auth.jwt() is the raw jwt decoded payload, it's unverified Maybe I misunderstood
garyaustin
garyaustin2h ago
The jwt is verified before auth.jwt() can use it.
Jacob
JacobOP2h ago
Oh so I'm free to use the auth.jwt fields in terms of security?
garyaustin
garyaustin2h ago
Yes. The JWT is verified by the signing key and all the claims are added to current settings by PostgREST. Then auth.jwt() reads those.
Jacob
JacobOP2h ago
Like for instance I have a buckets with paths of user ids I want to verify that user can upload only to his own bucket
garyaustin
garyaustin2h ago
Storage and Realtime use the same.
Jacob
JacobOP2h ago
so I configured the jwt with userid in the client using the custom callback and I just need to access userid from the verified jwt object in the RLS policy
garyaustin
garyaustin2h ago
Storage made a change ages ago because owner used to be just UUID from sub. Now there is a text version also.
Jacob
JacobOP2h ago
Interesting. I wonder then why you initially suggested trying to use the auth.uid()?
garyaustin
garyaustin2h ago
Because I did not know your auth provider was not use a UUID.
Jacob
JacobOP2h ago
Oh yea it's pretty odd And it would be cleaner to use auth.uid() Not sure about the syntax yet, will look into it. but I guess I can create some migration with helper function like betterauth_userid() that access the auth.jwt().user_id somehow and use it across all the RLS policies?
garyaustin
garyaustin2h ago
I would still use the sub claim for the user id even if text. I think that is needed for Storage to put it in the owner_id column.
No description
garyaustin
garyaustin2h ago
That is what Clerk does. They have/had a function to get the id from current_settings.
Jacob
JacobOP2h ago
btw no matter what I tried I couldn't access it from current_settings only from auth.jwt()
garyaustin
garyaustin2h ago
PostgREST 13.0
Transactions
After User Impersonation, every request to an API resource runs inside a transaction. The sequence of the transaction is as follows: Access Mode: The access mode determines whether the transaction can modify the database or not. There are 2 possible values: READ ONLY and READ WRITE. Modifying the...
garyaustin
garyaustin2h ago
They all get set into current settings for DB operations. I believe storage emulates that.
Jacob
JacobOP2h ago
you mean like I should try to be close to how it works with the regular format of supabase auth?
garyaustin
garyaustin2h ago
I would just use auth.jwt... But if you want the code I just linked gets you one step closer as that is what auth.jwt() calls.
Jacob
JacobOP2h ago
Nice so the JWT is even verified at postgres level
garyaustin
garyaustin2h ago
https://github.com/supabase/auth/blob/86b7de45c9432ea6ee9bd7c7e9cfe96e038fe2bc/migrations/20220531120530_add_auth_jwt_function.up.sql#L1 Auth is not actually involved in this. PostgREST (the db REST API), storage and realtime verify/decode the jwt claims into current settings as PostgREST does.
Jacob
JacobOP2h ago
btw if I use auth.jwt() then I don't need special fields, like I can even just pass only user_id? (whatever I want)
garyaustin
garyaustin2h ago
You have to have some minimum claims in the JWT for it to get validated. Role certainly.
Jacob
JacobOP2h ago
This is postgres requirement?
garyaustin
garyaustin2h ago
Postgres knows nothing about JWTs PostgREST does. It uses the role claim to set the user role to do the SQL request. This is anon,authenticated or service_role by default.
Jacob
JacobOP2h ago
Nice I will play with that and try to implement it now by using the auth.jwt() thank you! really appreciate your help.
Jacob
JacobOP2h ago
this is what I ended with
No description
No description
No description
No description
Jacob
JacobOP2h ago
Will be cool to add minimal example for how to use 3rd auths + RLS to the docs of Supabase
garyaustin
garyaustin2h ago
You could do a PR. I don't know if they have the bandwidth to add that to the docs.
Jacob
JacobOP2h ago
do you think this appraoch (which sadly i will have to use) is not totally stable with the othe features of supabase? or that it should work fine with the buckets/database since it's basic postgrest concepts
garyaustin
garyaustin2h ago
Sorry, I've only helped people doing this. I've never done custom JWT signing with SB myself. Realtime and Storage do not use PostgREST, but emulate the same methods to set the current_settings with the claims.

Did you find this page helpful?