Authenticated requests rejected by RLS as if unauthenticated — possibly related to recent JWT signing key rotation #47149
Replies: 4 comments
-
|
The useful next split is: is PostgREST receiving the user JWT at all, or is it receiving it and failing to trust the new signing key? Supabase's JWT docs say the Data API uses the JWT from I would add a temporary debug RPC and call it from the same deployed app/client that fails: create or replace function public.debug_request_jwt()
returns jsonb
language sql
stable
as $$
select jsonb_build_object(
'current_user', current_user,
'role_claim', current_setting('request.jwt.claim.role', true),
'sub_claim', current_setting('request.jwt.claim.sub', true),
'auth_uid', auth.uid()
);
$$;
grant execute on function public.debug_request_jwt() to anon, authenticated;Then: const { data, error } = await supabase.rpc('debug_request_jwt')
console.log({ data, error })Interpretation:
One thing also worth trying after rotation is forcing a fresh access token / session refresh or signing out/in. The signing-keys docs say rotation causes Auth to issue new JWT access tokens immediately, while old non-expired tokens should remain accepted; if that contract is broken, the RPC above should make it visible. Docs references: |
Beta Was this translation helpful? Give feedback.
-
|
If you look in the Gateway API log you should be able to see what the user role is in the detail data as decoded from the JWT. Note no one from Github is going to be able to check your project though only support can do that. |
Beta Was this translation helpful? Give feedback.
-
|
Yeah this is a known pain point after rotating from legacy HS256 to ECC (ES256). Your SQL editor test with What's probably going on After rotation Auth starts signing access tokens with ES256. PostgREST picks up the role for RLS from If PostgREST can't verify that ES256 token against the current JWKS (sync lag after rotation is the usual suspect), it often won't even give you a 401. It just treats you as That fits what you're seeing. GaryAustin1's point about reaching RLS is fair, but the JWT might still be getting handled as anon rather than authenticated.
First thing I'd do Drop in a debug RPC and call it from the same deployed client that's failing, not the dashboard. create or replace function public.debug_request_jwt()
returns jsonb
language sql
stable
security invoker
as $$
select jsonb_build_object(
'current_user', current_user,
'role_claim', current_setting('request.jwt.claim.role', true),
'sub_claim', current_setting('request.jwt.claim.sub', true),
'auth_uid', auth.uid(),
'auth_role', auth.role()
);
$$;
grant execute on function public.debug_request_jwt() to anon, authenticated;const { data: session } = await supabase.auth.getSession()
console.log('access_token alg:', JSON.parse(atob(session.session.access_token.split('.')[0])).alg)
const { data, error } = await supabase.rpc('debug_request_jwt')
console.log({ data, error })
const { error: insertError } = await supabase.from('your_table').insert({ /* your row */ })
console.log({ insertError })If you get back If you get anon with null claims, the live request isn't authenticated. Check headers, client setup, or a platform JWKS sync bug. If the RPC looks good but insert still fails, open Network tab and compare the two requests. Insert might be missing Authorization. Check the actual failing request In DevTools on the 42501 insert you want both headers present Authorization: Bearer eyJ...
apikey: eyJ...The bearer one must be Stuff I've seen after rotation Client still holding an old HS256 token in localStorage Worth forcing a clean session before retesting await supabase.auth.signOut()
await supabase.auth.signInWithPassword({ email, password })or Gateway logs Dashboard → Logs → API. Find the failed insert and check what role got decoded from the JWT in the log detail. If it says anon, you're not authenticated on that request even if Auth itself is happy. Token sanity check Decode your access token locally (don't post the full thing here). Header should show ES256, payload should have GET https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonand make sure the token kid matches something in there. If RPC returns anon but Authorization clearly has a valid ES256 token That's when I'd stop debugging in SQL and open a support ticket. Nobody on GitHub can poke your project infra. Send them project ref, roughly when you rotated, request id from logs, the RPC output, and the token alg/kid (not the full jwt). Mention that permissive insert to authenticated still 42501 over REST. Probably not the problem Your RLS SQL (you already proved that in the editor) Short version
Happy to look at your RPC output if you paste it (redact uuids). |
Beta Was this translation helpful? Give feedback.
-
|
Your instinct about the JWT signing key rotation is very likely right, and the "works in the SQL editor with claims set manually, fails from the live app" split is the tell. Here's the mechanism: After a rotation from HS256 → ECC (P-256), the usual causes are:
Start with #2 — the |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
After my project's JWT signing key rotated from Legacy HS256 to ECC (P-256)
(shown in Settings → JWT Keys as rotated ~5 hours before this issue started),
authenticated REST requests to PostgREST are being rejected by Row Level
Security as if no user is logged in — even though the same session token is
confirmed valid by the Auth service.
Steps to reproduce:
the correct user id with no error — confirming the access token is valid.
PostgREST request using the same session is rejected with:
{"code":"42501","message":"new row violates row-level security policy
for table ..."}
Isolation steps already taken:
pg_policiesthat the relevant RLS policies exist exactlyas intended.
set local role authenticated;andset local request.jwt.claims = '...'with the real user's UUID —
select auth.uid()correctly returned thatuser's id, and
current_usercorrectly showedauthenticated.permissive rule —
for insert to authenticated with check (true)— STILLresults in the same rejection from the live application (not the SQL
editor), strongly suggesting that real PostgREST requests from the
deployed app are not being recognized as the
authenticatedrole at all,even though the same token verifies successfully via the Auth service.
This strongly suggests PostgREST/Data API is not correctly validating or
recognizing JWTs signed with the new ECC (P-256) key, possibly due to a
caching or propagation issue following the automatic key rotation.
Could you please check whether the Data API / PostgREST service for this
project is correctly synced to the current JWT signing key? Happy to
provide further logs or a minimal repro if useful.
Beta Was this translation helpful? Give feedback.
All reactions