Vai al contenuto

Basi della sicurezza

Supabase "invalid claim: missing sub claim": nessun utente connesso

"invalid claim: missing sub claim": Supabase Auth ha ricevuto un token autentico senza alcun utente, la chiave del progetto al posto di una sessione.

Vlad Tkachenko12 min di lettura
Un badge appeso a un laccio, con un sigillo autentico e la riga del nome vuota, davanti a due badge che hanno un nome e un volto.

In breve

  • "invalid claim: missing sub claim" è Supabase Auth che risponde 403 con il codice bad_jwt. Gli hanno chiesto chi sta chiamando, il token ricevuto era autentico e non nominava nessuno.
  • La pagina di troubleshooting di Supabase indica la causa comune: una chiave API mandata dove va il token di accesso di un utente che ha fatto l'accesso. Una chiave dice quale app chiama e non contiene nessuna persona.
  • Chiama la funzione solo quando qualcuno ha fatto l'accesso, e falle rispondere 401 quando non c'è nessuno. Mettere al suo posto la chiave segreta non cambia nulla, perché anche quella non nomina nessuno.

La tua app ha una Supabase Edge Function che fa qualcosa per la persona che la usa, come salvare le sue impostazioni o chiamare OpenAI per conto suo. Mentre la provavi funzionava. Poi il log della funzione comincia a riempirsi di questa riga, e l'app mostra un errore o niente del tutto:

AuthApiError: invalid claim: missing sub claim

Cerca "invalid claim: missing sub claim" e ti diranno di rigenerare le chiavi, di spegnere Verify JWT o di mettere la chiave service_role. Ognuno di questi consigli tratta il token come rotto. Il token andava bene. Supabase ha controllato la firma e l'ha accettato. Quello che gli mancava era un nome, qualunque.

Cosa significa "invalid claim: missing sub claim"?

A Supabase Auth è stato chiesto quale utente sta chiamando; ha trovato il token autentico e nessun utente scritto dentro.

I token che legge Supabase Auth sono JWT: tre blocchi di testo separati da punti, in cui il blocco centrale sono dati leggibili. Ogni voce di quei dati si chiama claim. sub, abbreviazione di subject, è il claim che contiene l'id di un utente, e la guida ai JWT di Supabase lo descrive come l'ID dell'utente, un UUID.

Il messaggio è scritto nel codice sorgente di Supabase Auth stesso, in una funzione chiamata maybeLoadUserOrSession dentro internal/api/auth.go. Quando quella funzione gira, la firma del token è già stata controllata ed è passata. Poi cerca sub, e quando il claim è vuoto si ferma con un HTTP 403, il codice di errore bad_jwt e questa frase. La risposta che riceve il tuo codice è così:

{"code":403,"error_code":"bad_jwt","msg":"invalid claim: missing sub claim"}

Immagina una reception a cui viene fatta una sola domanda: che visitatore è questo? Porgi un badge. La reception controlla prima il timbro, e il timbro è autentico. Poi legge la riga del nome, e la riga del nome è vuota. La reception non ha niente con cui rispondere, quindi rifiuta, e il rifiuto indica la riga vuota.

Qualsiasi chiamata che abbia bisogno di una persona che ha fatto l'accesso può ricevere questa risposta. getUser() e updateUser() sono le due in cui è più probabile che tu la veda, e anche aggiungere un secondo fattore o collegare un altro metodo di accesso passa dallo stesso controllo.

Il sigillo si controlla per primo, ed entrambi i badge passano. La domanda che fallisce è la seconda: di chi è il nome che porta.

Una chiave nomina la tua app. Un token di sessione nomina una persona.

La chiave API del tuo progetto è il badge con la riga del nome vuota, ed è stata fatta così apposta.

La documentazione delle chiavi API lo dice in una riga: secondo Supabase, una chiave API non distingue tra utenti, ma tra applicazioni. La chiave dice a Supabase quale app sta chiamando. Chi sta usando quell'app è un'altra domanda, a cui risponde un token che Supabase Auth rilascia a ogni persona quando fa l'accesso.

La guida ai JWT di Supabase li riporta entrambi. La chiave anon di un progetto più vecchio si decodifica così, e non c'è nessun sub da nessuna parte:

{
  "iss": "supabase",
  "ref": "abcdefghijklmnopqrst",
  "role": "anon",
  "iat": 1640991600,
  "exp": 1640995200
}

Il token di accesso di una persona che ha fatto l'accesso porta il suo id, il ruolo authenticated e la sessione a cui appartiene (ridotto qui alle righe che contano):

{
  "sub": "123e4567-e89b-12d3-a456-426614174000",
  "role": "authenticated",
  "session_id": "123e4567-e89b-12d3-a456-426614174000",
  "email": "user@example.com"
}

La chiave service_role è di nuovo la prima forma, con "role": "service_role" e ancora nessun sub. Le chiavi più recenti sb_publishable_ e sb_secret_ non sono nemmeno JWT, ma la pagina di troubleshooting di Supabase per questo errore dice che Auth le riconosce e che neanche loro portano il claim sub. Quindi ogni chiave del tuo progetto, vecchia o nuova, pubblica o segreta, produce questo messaggio quando viene consegnata ad Auth come se fosse una persona. La stessa pagina nomina lo sbaglio senza giri di parole: usare per distrazione una chiave API di Supabase al posto del token di accesso di Auth.

Se le chiavi e i loro prefissi sono una novità per te, quali chiavi API possono stare nel tuo frontend spiega come distinguere quella pubblica da quella segreta.

Dove la chiave viene mandata al posto di una sessione

Tre strade portano qui, e la prima è quella che prende una Edge Function.

1. Una Edge Function chiamata da una pagina in cui nessuno ha fatto l'accesso. Quando supabase-js ha una sessione, mette il token della persona nell'header Authorization. Quando non ce l'ha, non ha nient'altro da metterci, quindi con una chiave vecchia manda in quell'header la chiave del tuo progetto. Il suo file fetch.ts si legge esattamente così: il token di sessione se c'è, altrimenti la chiave.

Il controllo Verify JWT della piattaforma lascia entrare quella richiesta. Una chiave vecchia è un JWT firmato dal tuo progetto, e Supabase documenta che il controllo accetta anche le chiavi nuove: è la falla che spostare un segreto in una Edge Function spiega per intero. La tua funzione passa poi l'header ad Auth, ed è quello che fanno queste righe:

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_ANON_KEY')!,
  { global: { headers: { Authorization: req.headers.get('Authorization')! } } },
)
const { data: { user } } = await supabase.auth.getUser()

Auth legge la chiave, trova la riga del nome vuota e risponde 403. Se la funzione restituisce quel fallimento, la tua app mostra Edge Function returned a non-2xx status code, con questo messaggio che aspetta nel log della funzione.

2. Una chiamata fatta prima che l'accesso fosse completato. La pagina di troubleshooting di Supabase nomina anche questo caso: un utente che aveva cominciato a registrarsi o ad accedere e non aveva finito quando è stato chiamato getUser. Una forma che prende è una registrazione che aspetta ancora la sua email di conferma. Non c'è ancora una sessione, quindi quello che è arrivato ad Auth non era il token di una persona.

3. Il tuo server passa ad Auth il valore sbagliato. Una route che chiama getUser(token) con la chiave in token, oppure un'automazione in n8n, Make o Postman che chiama /auth/v1/user con la chiave nel suo header Authorization. La soluzione che dà la pagina di Supabase per questo caso è assicurarsi che il token passato a getUser() sia il token di accesso della persona.

Senza nessuno che abbia fatto l'accesso, la pagina manda la stessa chiave in entrambi gli header. Ogni controllo prima di Auth la lascia passare, e solo la domanda di Auth trova vuota la riga del nome.

Come vedere cosa ha ricevuto la tua funzione

Confronta due header della richiesta nel tuo browser.

Apri la tua app pubblicata, premi F12, scegli la scheda Network e ripeti ciò che è fallito. Clicca sulla richiesta il cui indirizzo contiene /functions/v1/ e il nome della tua funzione, e leggi i suoi header di richiesta:

  • Authorization contiene Bearer e poi la stessa stringa di apikey. La pagina non aveva una sessione da mandare, quindi ha mandato la chiave due volte. Sei sulla strada 1 o 2.
  • Authorization contiene un'altra stringa lunga che comincia con eyJ. Il token di una persona è partito. La funzione o il tuo server l'ha perso per strada, quindi sei sulla strada 3.
  • Non c'è nessun header Authorization. Allora non è questo il messaggio che vedrai. Con Verify JWT acceso, la piattaforma rifiuta la richiesta con un 401 prima che giri il tuo codice, e l'articolo sull'errore non-2xx lo legge. Spento, Auth risponde This endpoint requires a valid Bearer token, la prima riga della tabella più sotto.

Se invece il messaggio è nella console del browser e dice Auth session missing!, nessuna richiesta ha raggiunto Auth: supabase-js non ha trovato nessuna sessione e si è fermata prima di mandarne una, e lo tratta il suo articolo.

Leggi i valori dove sono. Non incollare nessuno dei due in un sito che si offre di decodificarlo: la tua chiave è comunque pubblica, ma il token di accesso di una persona permette a chiunque lo abbia di agire come quella persona finché non scade.

La soluzione: chiedere solo quando qualcuno ha fatto l'accesso

Decidi cosa deve fare la funzione per un visitatore che non ha fatto l'accesso, e scrivi quella decisione nella funzione.

Se devono usarla solo le persone che hanno fatto l'accesso, due modifiche. La pagina chiama la funzione solo dopo che qualcuno ha fatto l'accesso. E la funzione risponde un 401 netto quando getUser() non trova nessun utente, così una chiamata vagante da una pagina senza accesso riceve un rifiuto chiaro invece di un crash:

const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  return new Response(JSON.stringify({ error: 'Sign in first' }), {
    status: 401,
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
}

Tieni gli header CORS anche su quella risposta, altrimenti il browser la nasconde alla tua app e ti trovi davanti "Failed to send a request to the Edge Function" al suo posto. Supabase offre anche un wrapper, withSupabase({ auth: 'user' }), che fa lo stesso controllo prima che giri il tuo handler; l'articolo sulla Edge Function mostra come impostarlo.

Se devono usarla anche i visitatori senza accesso, chiedi ad Auth solo sulla strada che ha bisogno di una persona. Tratta un rifiuto lì come "questo è un visitatore" e vai avanti con ciò che un visitatore può fare, invece di lasciare che l'errore fermi la richiesta.

Sul tuo server, manda il token di accesso della persona dal browser, come descrive la pagina di Supabase: mettilo in un header Authorization: Bearer della tua richiesta, leggilo sul server e chiama supabase.auth.getUser(token) con quel valore. La chiave del tuo progetto non entra mai in quella chiamata.

Due correzioni che peggiorano le cose

Entrambe vengono suggerite per questo errore, ed entrambe ti lasciano messo peggio dell'errore stesso.

Mettere al suo posto la chiave segreta. Non può aiutare, perché service_role e sb_secret_ non portano sub nemmeno loro, quindi Auth risponde allo stesso modo. Il danno sta in dove finisce la chiave. Chiedi a un builder di "usare la chiave di servizio" per superare un errore e potrebbe benissimo metterla nella pagina che i tuoi visitatori scaricano, dove legge e scrive ogni riga del tuo database qualunque cosa dicano le tue regole. Se è già successo, ruotala prima di tutto.

Questo passo falso si vede dall'esterno. La nostra scansione gratuita legge il JavaScript della tua app pubblicata come farebbe un estraneo e nomina ogni chiave che trova, compreso se è una che deve stare lì. Ci mette circa 20 secondi e non serve un account: scansiona la tua app.

Togliere il controllo e prendere l'id dell'utente dalla richiesta. Una funzione che legge userId dal corpo della richiesta crede a chiunque lo mandi. Se quella funzione scrive anche con la chiave segreta, chiunque apra la tua pagina e legga come viene chiamata la funzione può agire come uno qualsiasi dei tuoi utenti, e niente nei tuoi log dice che non erano loro. Con il controllo al suo posto, un estraneo che prova la stessa chiamata riceve questo errore e nient'altro.

Gli altri messaggi dello stesso controllo

I controlli di Auth girano in un ordine fisso e ognuno ha la sua frase, quindi il messaggio ti dice fin dove è arrivato il token.

MessaggioStato e codiceCosa è successo
This endpoint requires a valid Bearer token401 no_authorizationNon è arrivato nessun header Authorization: Bearer.
invalid JWT: unable to parse or verify signature, …403 bad_jwtIl timbro è sbagliato: un token rovinato, uno di un altro progetto o una chiave vecchia.
invalid claim: missing sub claim403 bad_jwtIl timbro è giusto e la riga del nome è vuota: una chiave API.
invalid claim: sub claim must be a UUID400 bad_jwtUn nome c'è, ma non nella forma di un id utente di Supabase.
User from sub claim in JWT does not exist403 user_not_foundIl token nomina un utente che nel frattempo è stato eliminato.
Session from session_id claim in JWT does not exist403 session_not_foundIl token è sopravvissuto alla sua sessione, per esempio dopo un'uscita su un altro dispositivo.

Ogni riga viene dallo stesso file, internal/api/auth.go. La versione che dà il tuo database per la riga del timbro sbagliato, PGRST301, ha un articolo tutto suo. La riga della sessione va letta insieme a perché gli utenti di Supabase vengono disconnessi.

Tenere il controllo al suo posto dopo la prossima modifica

Un controllo su chi sta chiamando può sparire la prossima volta che chiedi al tuo builder di far sparire un errore, e i due modi più rapidi per zittire questo sono i due qui sopra.

Reeve Monitor scansiona di nuovo la tua app pubblicata ogni ora:

  • tutti e nove i controlli ogni ora, su un massimo di tre app
  • una email il giorno in cui una nuova scansione valuta la tua app peggio della precedente; una chiave segreta nel tuo JavaScript limita da sola il voto a D
  • se l'app risponde, ogni 60 secondi
  • un rapporto mensile di quello che ha visto

Una funzione che crede all'id utente che le viene mandato può cambiare le righe di uno qualsiasi dei tuoi utenti per chiunque la chiami. Reeve Care tiene una copia del tuo database Supabase di prima che succeda.

  • una copia cifrata secondo una pianificazione, conservata sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
  • ogni copia verificata prima di contare, contando le righe di ogni tabella
  • un ripristino in un clic che salva quello che c'è adesso prima di sostituirlo
  • anche i tuoi file caricati, appena colleghi una credenziale di Storage
  • tutto quello che fa Monitor

Cosa fare adesso

Cosa fare

  • Apri la richiesta che fallisce nella scheda Network. Se Authorization porta lo stesso valore di apikey, quando è partita nessuno aveva fatto l'accesso.
  • Chiama le funzioni che hanno bisogno di una persona solo dopo l'accesso, e fai rispondere la funzione 401 quando getUser() non trova nessun utente.
  • Sul tuo server, passa a getUser() il token di accesso della persona. La chiave del tuo progetto non contiene nessun utente.
  • Lascia la chiave segreta dov'è. Neanche lei nomina nessuno, e nella pagina legge e scrive ogni riga.
  • Tieni il controllo su chi sta chiamando. Una funzione che prende l'id dell'utente dalla richiesta crede a chiunque.

Se la tua app mostra Edge Function returned a non-2xx status code mentre questo messaggio è nel log, la funzione sta passando il rifiuto, e quell'articolo legge la risposta da lì in avanti. La checklist di sicurezza in 10 minuti copre cos'altro un'app appena lanciata tende a lasciare aperto.

FAQ

Cosa significa "invalid claim: missing sub claim" in Supabase?

Significa che a Supabase Auth è stato chiesto quale utente sta chiamando e gli è stato consegnato un token senza alcun utente dentro. Il token in sé era autentico e ha passato il controllo della firma. Gli mancava il claim sub, il campo che contiene l'id di un utente. Le chiavi API del tuo progetto sono fatte esattamente così, ed è per questo che la causa abituale è una chiave mandata dove va il token di un utente che ha fatto l'accesso.

È la stessa cosa di "Auth session missing!"?

No. "Auth session missing!" viene da supabase-js nel browser, che non trova nessuna sessione e non manda nemmeno la richiesta. "invalid claim: missing sub claim" torna da Supabase Auth, che ha ricevuto una richiesta e ha trovato una chiave dove doveva esserci il token dell'utente. Il primo indica di solito una pagina senza accesso; il secondo, una funzione o un server che ha passato la chiave.

Disattivare Verify JWT risolve?

No. Verify JWT è un controllo che Supabase fa su una Edge Function prima che parta il tuo codice, e questo messaggio arriva dopo, da Supabase Auth, quando il tuo codice chiede chi sta chiamando. Spegnere il controllo della piattaforma fa arrivare al tuo codice anche le richieste senza alcun token, e l'errore resta esattamente dov'era.

Un accesso anonimo causa questo errore?

No. Un accesso anonimo crea un utente vero con un id suo, quindi il suo token porta un claim sub e is_anonymous a true. La chiave anon è un'altra cosa, nonostante il nome simile: una chiave di progetto senza utente dentro, ed è lei a produrre questo messaggio.

Perché alcune pagine dicono 401 e altre 403?

La pagina di troubleshooting di Supabase per questo messaggio ha 401 nel titolo. Il codice attuale di Supabase Auth risponde 403, con il codice di errore bad_jwt. Se il tuo codice decide cosa fare in base al solo stato, controlla anche il codice di errore e il messaggio.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Fai il backup del database Supabase prima della prossima modifica

Reeve ne tiene una copia cifrata a intervalli programmati e la ripristina per te se qualcosa va storto.

Vedi i backup Supabase

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.