Saltar al contenido

Fundamentos de seguridad

Supabase invalid JWT: ¿qué clave dejó de funcionar?

Un error Supabase invalid JWT es PGRST301: un token firmado no pasó la verificación. En un proyecto nuevo se trata de la sesión, no de la clave de API.

Vlad Tkachenko12 min de lectura
Una carta cerrada con lacre delante de un sello cuyo grabado ya no coincide con el lacre, y más cartas detrás de una barra ámbar.

En resumen

  • Un error Supabase invalid JWT llega como PGRST301 con HTTP 401, y significa que la firma del token que iba en tu petición no se pudo verificar. No se buscó nada en una lista para luego rechazarlo.
  • Las claves publishable y secret son cadenas cortas sin firma, así que en un proyecto que ya se pasó a ellas este error habla de tu usuario con sesión iniciada y nunca de la clave de API.
  • PostgREST 13 cambió una frase vaga por una lista corta de frases concretas. Lee el mensaje que te llegó de verdad: cada uno descarta una causa distinta.

Ayer tu aplicación funcionaba. Esta mañana cada petición vuelve con un 401, en tu código no ha cambiado nada, y la respuesta habla de un JWT, algo que tú no has escrito nunca en ningún sitio.

{
  "code": "PGRST301",
  "message": "JWT cryptographic operation failed"
}

Un error Supabase invalid JWT es el que aparece después de hacer lo correcto. Rotaste una clave porque una guía te lo dijo, o pasaste tu proyecto a las claves nuevas, y la aplicación se quedó callada.

Y aquí es donde guía tras guía se equivoca: la clave de la que habla este mensaje a menudo no es la que acabas de cambiar. Busca el error y te mandarán a revisar tu clave anon. En un proyecto que ya se pasó a las claves publishable y secret, la clave de API no es un token firmado en absoluto, así que lo que no pasó la verificación era otra cosa de la petición.

¿Qué significa un error Supabase invalid JWT?

Algo de tu petición era un token firmado, y la firma no coincidió.

Una contraseña es una palabra que alguien busca en una lista. Un JWT no es eso. Es una carta cerrada con lacre: la persona de la puerta no te busca en ningún sitio, sostiene un sello y comprueba si el lacre de tu carta se hizo con él. Si es así, pasas, y nadie lee ninguna lista.

Una carta cerrada con el sello equivocado se rechaza aunque cada palabra de dentro sea correcta. Y en cuanto la puerta cambia de sello, ninguna carta que ya esté en camino se abre, sin que nadie haya tocado las cartas.

Tu petición es la carta. Tu proyecto de Supabase sostiene el sello. Un 401 aquí significa que los dos no coincidieron.

No se busca nada en ninguna lista. El lacre del token se compara con el sello que guarda tu proyecto, y una carta cuyo lacre lo hizo otra cosa se rechaza diga lo que diga por dentro.

El mensaje cambió en PostgREST 13 y el código no

Comprueba PGRST301. La frase que lo acompaña depende de la versión de PostgREST que ejecute tu proyecto, y las más nuevas te dan una lista corta de ellas.

La referencia de errores de PostgREST describe PGRST301 como un JWT recibido que no se pudo decodificar o que no es válido, con HTTP 401, tanto en la versión 13 como en la 14. Esa frase es la descripción de la tabla de documentación. No es lo que aterriza en tu campo message. Leyendo Error.hs en la rama main de PostgREST, ese campo lleva una de estas:

Lo que dice el mensajeLo que te dice
Empty JWT is sent in Authorization headerLa cabecera sí se envió y venía vacía.
Expected 3 parts in JWT; got 1Lo que llegó no es un JWT. Un JWT son tres bloques separados por puntos.
No suitable key or wrong key typeEl servidor no tiene ninguna clave capaz de verificar un token de ese tipo.
Wrong or unsupported encoding algorithmEl token nombra un algoritmo de firma que el servidor no usa.
JWT cryptographic operation failedEl token está bien formado y la firma no coincide.
Unsupported token typeEl token tiene una forma que PostgREST no lee.

JWT cryptographic operation failed es el que significa clave caducada. El token se construyó bien y lo firmó algo que el proyecto ya no tiene, que es exactamente lo que deja atrás una rotación.

Los proyectos antiguos ven algo muy distinto, y de ahí viene la cadena que aparece por todas partes en los resultados de búsqueda. Antes de la versión 13, PostgREST imprimía lo que había dicho su biblioteca de JWT, tal cual. De ahí sale JWSError JWSInvalidSignature: es un valor de biblioteca impreso en pantalla, no una frase que alguien escribiera para ti. El changelog de PostgREST de la 13.0.0 describe el cambio como mensajes de error diagnósticos en lugar de internos expuestos.

Vale la pena conocer un código vecino mientras estás aquí. PGRST303, también 401, es lo que recibes cuando la firma se verificó y el contenido del token fue rechazado: caducado, todavía no válido, o con la audiencia equivocada. Si ese es tu código, deja de mirar las claves y ve a mirar el reloj y la sesión.

¿Qué clave es: la de API o el token de tu usuario?

Depende de si tu proyecto todavía usa anon y service_role.

La documentación de Supabase sobre claves de API es explícita en la diferencia. Las claves antiguas anon y service_role son JWT de larga duración, y por eso empiezan por eyJ. Las claves publishable y secret más nuevas son cadenas cortas y no son JWT en absoluto.

Ese único hecho parte el error en dos:

  • Tu proyecto sigue usando anon y service_role. La clave de API de tu petición es en sí misma un token firmado, así que una clave de otro proyecto, una clave de antes de la rotación o un pegado truncado fallan la verificación de firma igual que lo haría una sesión mala. Cualquiera de los dos tokens de la petición puede ser el que falló.
  • Tu proyecto ya usa sb_publishable_ y sb_secret_. La clave de API ha dejado de ser un token, así que no hay ninguna firma en ella que algo pueda rechazar. Lo que queda por verificar es el token de acceso de tu usuario con sesión iniciada, que Supabase Auth emite aparte de la clave de API.
En un proyecto antiguo llegan dos cartas lacradas y cualquiera de las dos puede ser la rechazada. Tras pasar a las claves nuevas, la clave de API ya no lleva lacre, y la sesión de tu usuario es lo único que conserva una firma.

En la práctica, la primera pregunta no es qué clave está mal. Es si había alguien con la sesión iniciada cuando falló la petición. Si una página sin sesión funciona y una con sesión no, estás mirando sesiones. Si falla todo, incluida una página que no pide nada personal, estás mirando la clave de API, y tu proyecto es de los antiguos.

Si son sesiones y tu log de Auth dice además Invalid Refresh Token, a tus usuarios se les cierra la sesión porque terminó la sesión detrás de su token.

Si la respuesta vino de Supabase Auth y dice invalid claim: missing sub claim, el token pasó la comprobación de su firma y no nombraba a nadie. Es una clave enviada donde va el token de una persona, y ese mensaje tiene su propio artículo.

Invalid JWT no es "No API key found in request"

Son dos mensajes distintos con dos causas distintas, y las soluciones no se parecen en nada.

Un invalid JWT significa que llegó un token y la firma no coincidió. "No API key found in request" significa que no llegó ninguna cabecera apikey, así que la petición se rechazó en la pasarela antes de preguntar nada a tu base de datos.

La propia respuesta aclara cuál tienes. Una clave que falta la rechaza la pasarela que hay delante de tu proyecto, así que el cuerpo lleva un message y un hint y ningún code. Un invalid JWT llegó lo bastante lejos como para que lo leyera PostgREST, y por eso vuelve con PGRST301 escrito.

Dónde sigue estando la clave antigua, por orden

Cuatro sitios, y conviene mirarlos en este orden porque es más o menos la frecuencia con la que cada uno es la respuesta.

  1. Los ajustes de proyecto de tu herramienta. Lovable, Bolt, v0 y Replit guardan cada uno su propio juego de variables de entorno, y si construiste con Cursor o Claude Code ese mismo juego está donde despliegues. Es la copia que casi todo el mundo actualiza, y la única que casi todo el mundo actualiza.
  2. Una clave escrita directamente en el código. Viaja dentro del JavaScript que descargan tus visitantes, así que cambiarla en el panel no cambia nada hasta que se edite el archivo y se reconstruya el sitio. Dónde está esa página y qué hace cada valor, si no la has abierto.
  3. Los secretos de Edge Function. Una función guarda su propio juego, separado del de tu frontend, así que sigue usando la clave antigua cuando todo lo demás ya ha cambiado. Supabase también advierte de que una función con el ajuste Verify JWT activado puede romperse al rotar una clave de firma.
  4. Un despliegue que nadie ha reconstruido. La clave quedó incrustada en la última construcción, así que la copia en producción la sigue enviando hasta que algo dispare una nueva. Tu panel enseña la clave nueva todo el rato, y eso es lo que hace difícil ver este caso: el sitio donde vas a comprobarlo es el único que ya está bien.

Qué clave envía de verdad tu aplicación en producción

Abre el sitio publicado en lugar de la vista previa y lee qué envía la página.

Una clave antigua de Supabase es una cadena larga que empieza por eyJ. Las nuevas se anuncian en sus primeros caracteres: sb_publishable_ va en tu aplicación y sb_secret_ nunca. Si encuentras la segunda en una página que puede cargar un visitante, deja este artículo y rótala antes de hacer nada más. Cuál de las dos tienes, y qué cambió cuando Supabase les puso nombres nuevos, es lo que conviene resolver antes de empezar a sustituir valores.

Para ver qué emite tu proyecto ahora mismo: todas las claves viven ya en un solo sitio del panel, en Settings y luego API Keys, las antiguas incluidas.

Si prefieres no leer un bundle minificado, nuestro escaneo gratuito lee tu aplicación en producción como lo haría un desconocido y te dice qué claves se ven desde fuera. Tarda unos 20 segundos y no necesita cuenta: escanea tu aplicación.

Rotar una clave de firma sin cerrar la sesión de todos

Cuál de los dos caminos de rotación tomes decide si tus usuarios lo notan.

La documentación de Supabase sobre claves de firma los pone uno al lado del otro. Rotar el secreto JWT antiguo cierra la sesión de todos los usuarios activos de inmediato, y anon y service_role hay que rotarlas a la vez. Las JWT signing keys más nuevas funcionan de otra manera: una clave nueva empieza como standby, al rotar pasa a ser la actual, y la que sustituye pasa a ser la anterior. Mientras eso sea así, se siguen aceptando las firmas de las dos claves, y las cartas lacradas la semana pasada todavía se abren. Revocar la clave antigua es el paso aparte que pone fin a eso.

Dos avisos de esa misma documentación son los que pillan a la gente:

  • Un componente que verifica los tokens por su cuenta contra el secreto JWT antiguo, con una biblioteca como jose o jsonwebtoken, se rompe al rotar. Sostiene el sello viejo y nadie le ha dicho que lo cambie.
  • Una Edge Function con Verify JWT activado puede romperse por lo mismo.

Crear claves publishable y secret tampoco revoca tus claves antiguas. Siguen funcionando hasta que las desactivas, algo que la documentación señala como un paso aparte. Así que una aplicación que solo ha movido la mitad de sus copias sigue funcionando durante semanas, y el día en que alguien desactiva el par antiguo es el día en que la otra mitad empieza a responder 401.

Vigilar una aplicación que ha empezado a responder 401

Una aplicación que devuelve 401 a cada petición sigue en pie, sigue sirviendo una página y sigue respondiendo a cualquier comprobación de salud. En tu panel no se pone nada en rojo. Te enteras cuando alguien te escribe.

Reeve Monitor se entera primero:

  • las nueve comprobaciones cada hora, en hasta tres aplicaciones
  • un correo el día en que un nuevo escaneo puntúa tu aplicación peor que el anterior
  • si la aplicación responde, cada 60 segundos
  • un informe mensual de lo que vio

Una clave que tuviste que rotar con prisa suele ser una clave que estaba donde no debía, y desde aquí no puedes ver qué se leyó o se cambió con ella mientras estuvo fuera. Reeve Care guarda una copia de tu base de datos de Supabase anterior a eso.

  • una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
  • cada copia verificada antes de contar, contando las filas de cada tabla
  • una restauración con un clic que guarda lo que hay ahora antes de reemplazarlo
  • también tus archivos subidos, en cuanto conectes un acceso de Storage
  • todo lo que hace Monitor

Qué hacer hoy

Qué hacer

  • Lee el campo code, no la frase. PGRST301 es una firma que no coincidió, PGRST303 es un token cuyo contenido fue rechazado, por ejemplo uno caducado.
  • Averigua qué token falló antes de cambiar ninguna clave. Si las páginas sin sesión funcionan y las que tienen sesión no, es una sesión y no tu clave de API.
  • Revisa los cuatro sitios donde se esconde la clave antigua: los ajustes de la herramienta, el propio código, los secretos de Edge Function y un despliegue que nadie reconstruyó.
  • Haz la rotación de una sentada. Cada copia que dejes atrás es una aplicación que funciona a medias, y el sitio en producción sigue enviando la clave antigua hasta que se reconstruya.
  • Con las claves de firma nuevas, revocar es el paso que acaba de verdad con las firmas antiguas. Rotar por sí solo las deja aceptadas.

Empieza preguntándote si una página sin sesión todavía carga, porque esa sola respuesta decide qué mitad de este artículo te afecta. Si resulta que había una clave secreta a la vista, rotarla explica en qué orden proceder, y qué claves fueron alguna vez seguras de publicar cuenta por qué las dos acabaron pareciéndose tanto.

Preguntas frecuentes

¿Qué significa PGRST301?

PGRST301 es PostgREST respondiendo HTTP 401 porque no pudo verificar un token firmado que iba en tu petición. PostgREST es el servidor que convierte tus tablas de Supabase en la API HTTP con la que habla tu aplicación. Su propia referencia de errores describe el código como un JWT recibido que no se pudo decodificar o que no es válido, y desde la versión 13 el campo message lleva una frase más concreta sobre qué parte de la comprobación falló.

¿Por qué recibo 401 después de rotar mi clave de Supabase?

Porque en algún sitio se sigue enviando una copia de la clave antigua. Los lugares habituales son los ajustes de proyecto de tu herramienta, una clave escrita directamente en el código, un secreto de Edge Function y un despliegue que se construyó antes del cambio y que nadie ha vuelto a construir. Un token firmado con una clave que el servidor ya no acepta no pasa la verificación, y eso te llega como un 401 en cada petición en vez de como un aviso sobre la clave.

¿En qué se diferencia de No API key found in request?

Son dos mensajes distintos con causas distintas. No API key found in request significa que la petición llegó sin ninguna cabecera apikey y la rechazó la pasarela antes de que se preguntara nada a tu base de datos. Un invalid JWT significa que el token sí estaba y su firma no coincidió. Lo primero es una carta sin dirección, lo segundo una carta con el lacre equivocado.

¿Tengo que actualizar también mis Edge Functions?

Sí, y es la copia que más se olvida, porque una Edge Function guarda sus propios secretos, separados de los que usa tu frontend. Supabase también advierte de que una función con el ajuste Verify JWT activado puede romperse al rotar una clave de firma, y de que cualquier componente que verifique los tokens por su cuenta contra el secreto JWT antiguo se romperá por el mismo motivo.

¿Cómo veo qué clave envía realmente mi aplicación en producción?

Abre tu sitio publicado, no la vista previa, y mira qué envía la página de verdad. Una clave antigua de Supabase es una cadena larga que empieza por eyJ, y las nuevas empiezan por sb_publishable_ o sb_secret_. Nuestro escaneo gratuito lee una aplicación en producción como lo haría un desconocido y te dice qué claves se ven desde fuera, lo que también revela si todavía se está enviando una antigua.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

Haz una copia de tu base de datos de Supabase antes del próximo cambio

Reeve la guarda cifrada con regularidad y la restaura por ti si algo falla.

Ver copias de Supabase

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.