401 invalid claim: missing sub claim
getUser() fails with "invalid claim, missing sub claim" because the token it checked is the anon key, not a user's access token. How to spot it in servers and Edge Functions.
What you see
A call to supabase.auth.getUser() fails, most often in server code or an Edge Function:
AuthApiError: invalid claim: missing sub claim
The response has error_code: "bad_jwt". Depending on the Auth version and how your code reports it, the status is 401 or 403.
Why it happens
getUser() sends a token to Auth and asks which user it belongs to. A user's access token carries that user's id in its sub claim. The anon and service_role keys are also JWTs, but they have no sub, because they don't represent a user. Pass one of them to getUser() and Auth can't name a user, so it rejects the token.
Common ways a key ends up in place of a user token:
- Nobody is signed in. The client then sends the anon key in the
Authorizationheader, and your server or function forwards that header togetUser(). supabase.functions.invoke()ran before sign-in finished, so the request carried the anon key.- Your code passed the key, not
session.access_token, togetUser(token). - A server client was created with the
service_rolekey andgetUser()was called on it without a token.
How to fix it
Treat it as "not signed in." In a function or API route, return 401 instead of letting the error bubble up as a 500:
import { createClient } from "npm:@supabase/supabase-js@2"; Deno.serve(async (req) => { const token = req.headers.get("Authorization")?.replace("Bearer ", "") ?? ""; const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, { global: { headers: { Authorization: `Bearer ${token}` } } }, ); const { data, error } = await supabase.auth.getUser(token); if (error || !data.user) { return Response.json({ error: "Sign in to save recipes" }, { status: 401 }); } const { data: saved } = await supabase .from("saved_recipes") .select("recipe_id") .eq("user_id", data.user.id); return Response.json(saved); });Only call user-only endpoints with a session. On the client, check first:
const { data: { session } } = await supabase.auth.getSession(); if (!session) return showSignIn(); await supabase.functions.invoke("saved-recipes");Pass the access token, not a key, when you call
getUser(token)yourself.Keep admin and user clients separate. A client built with
service_roleis for admin work. To find out who is calling, check the caller's token with a client built from the anon key, as in step 1.
On SupaHost
An Edge Function's Require JWT toggle (on by default) checks that the request carries a valid JWT signed with the project's secret. The anon key passes that check. So a request can get through the gateway and still have no user behind it. If a function acts for a user, call getUser() in the function as shown above.