Saltar al contenido

Fundamentos de seguridad

Supabase "Auth session missing!": adónde fue la sesión

"Auth session missing!" es supabase-js sin encontrar ninguna sesión iniciada y sin llegar a preguntar a Supabase. Adónde fue la sesión, y el arreglo.

Vlad Tkachenko12 min de lectura
Una funda de credencial vacía colgada de una cinta, con el hueco de la credencial marcado en ámbar, delante de dos credenciales con cara.

En resumen

  • "Auth session missing!" (AuthSessionMissingError, estado 400) sale de supabase-js, dentro de tu aplicación. La biblioteca buscó una sesión iniciada, no encontró ninguna y nunca envió la petición a Supabase.
  • En una página donde nadie ha iniciado sesión es la respuesta correcta, y tu código solo tiene que tratarla. En una página para restablecer la contraseña significa que el enlace no inició la sesión de nadie aquí. En una Edge Function o en un servidor, la sesión del visitante nunca llegó.
  • Deja getUser() donde está. Cambiarlo por getSession() en un servidor hace desaparecer el error creyéndose lo que diga la cookie.

Tu aplicación de Lovable o Bolt inicia la sesión de la gente con Supabase. Una mañana abres la consola del navegador y encuentras esto en rojo, en una página donde nada parece ir mal:

AuthSessionMissingError: Auth session missing!

O una clienta te escribe para decirte que la página "Elige una contraseña nueva" la rechaza con esas mismas palabras, cada vez.

Busca "auth session missing supabase" y encontrarás hilos llenos de arreglos: cambiar getUser() por getSession(), bajar de versión la biblioteca, añadir una espera, reiniciar el proyecto. Dan por hecho que algo se rompió entre tu aplicación y Supabase. No se hizo ninguna petición. El error se escribe dentro de tu aplicación, lo escribe la biblioteca de Supabase, antes de que nada salga del navegador.

¿Qué significa "Auth session missing!" en Supabase?

A supabase-js le preguntaron quién había iniciado sesión, buscó una sesión, no encontró ninguna y respondió por su cuenta.

Una sesión es lo que Supabase Auth le da a una persona cuando inicia sesión: un par de tokens que dicen quién es, guardados por el navegador para que la siguiente carga de página la siga reconociendo. supabase-js es la biblioteca con la que tu aplicación habla con Supabase, y cada llamada que necesita a una persona empieza por sacar esa sesión.

El mensaje está escrito en el propio supabase-js, en el archivo errors.ts de la biblioteca de autenticación: un AuthSessionMissingError con el texto Auth session missing! y el estado 400. Ese 400 es una etiqueta que se pone la propia biblioteca. Ningún servidor respondió con él, y los registros de Supabase no mostrarán nada, porque getUser() busca una sesión antes de hacer cualquier otra cosa. Un comentario en su código lo dice: devuelve un error si no hay ni token de acceso ni una cabecera Authorization propia.

Imagina una recepción que sabe decirte qué visitante es cada uno. A cada persona que inicia sesión le dan una credencial en la puerta, y supabase-js guarda esa credencial en el bolsillo del navegador. Cuando tu código pregunta "¿quién es este?", supabase-js mira primero en el bolsillo. Si hay una credencial, la lleva a recepción y vuelve con un nombre. Si el bolsillo está vacío, ni siquiera va a recepción. Vuelve en el acto y te dice que no hay credencial.

Con una sesión, supabase-js pregunta a Supabase Auth. Sin ella, se detiene en tu aplicación, y el error sale de ahí.

Se devuelve, no se lanza

getUser() devuelve el error junto a un usuario vacío, y tu código decide qué pasa después.

La llamada es así, y sin sesión user queda en null y error contiene el AuthSessionMissingError:

const { data: { user }, error } = await supabase.auth.getUser()

No se lanza nada, así que un try … catch alrededor de la llamada no atrapa nada. De ahí viene el daño visible, con una de dos formas:

  • El código muestra cualquier error que recibe. Un visitante que aún no ha iniciado sesión ve "Auth session missing!" en un aviso o en un recuadro rojo, en una página que tenía derecho a ver.
  • El código usa el usuario sin comprobarlo. Lee user.id y la página se rompe con Cannot read properties of null, que parece otro fallo distinto.

Las dos son el mismo error: tratar "nadie ha iniciado sesión" como una avería. Comprueba primero el usuario, y dale a un visitante sin sesión la pantalla pensada para él:

const { data: { user } } = await supabase.auth.getUser()
if (!user) {
  // nadie ha iniciado sesión: mostrar la pantalla de acceso o la página pública
  return
}

Si quieres distinguir esto de una avería de verdad, la biblioteca le pone nombre: error.name es 'AuthSessionMissingError'.

Dónde se pierde la sesión

En tres sitios, y solo el primero es inofensivo.

1. Nadie ha iniciado sesión en este navegador. Tu aplicación llama a getUser() en una página que cualquiera puede abrir, quizá para decidir si muestra "Iniciar sesión" o "Mi cuenta". Para un visitante, el bolsillo vacío es la respuesta verdadera. Aquí es donde la mayoría se encuentra el mensaje, y el arreglo de código de arriba es todo lo que necesita.

2. Una página para restablecer la contraseña o de enlace mágico. Poner una contraseña nueva llama a updateUser({ password }), y la guía de contraseñas de Supabase es clara con el orden: es una vez que hay sesión cuando se recoge la contraseña nueva y se llama a updateUser. En esa página, la sesión tiene que venir del enlace del correo, que inicia la sesión de la persona al abrirse. Cuando no lo hace, el bolsillo está vacío y updateUser() responde "Auth session missing!". Las razones habituales:

  • El enlace ya se había usado, o había caducado, antes de abrirlo. Por qué Supabase dice que el enlace del correo no es válido o ha caducado repasa cada una.
  • Se abrió en otro navegador o aplicación distinto del que lo pidió. Cuando una aplicación usa el inicio de sesión PKCE de Supabase, parte del intercambio se queda en el navegador que lo empezó, y según la documentación de Supabase el intercambio del código tiene que iniciarse en el mismo navegador y dispositivo en que empezó el proceso.
  • Mandó a la persona a otro sitio, como localhost, así que la sesión llegó a una dirección que no es tu aplicación. Por qué Supabase redirige a localhost cubre ese caso.

3. Tu servidor o una Edge Function nunca recibió la sesión. La credencial está en el navegador de tu visitante. El código que corre en otra parte, una Edge Function de Supabase o la mitad de servidor de una aplicación Next.js hecha con v0 o Cursor, crea su propio cliente de Supabase, y un cliente nuevo empieza con el bolsillo vacío. Mientras no le pases la sesión del visitante, getUser() no encuentra nada allí y responde "Auth session missing!" sin preguntar a Supabase.

En una Edge Function la sesión viaja en la cabecera Authorization de la petición, y supabase-js solo la usa si se la entregas al crear el cliente. En una aplicación Next.js viaja en cookies, que la herramienta de servidor de Supabase solo lee si le das las de la petición. Si una Edge Function devuelve ese fallo como error, tu aplicación muestra "Edge Function returned a non-2xx status code", con este mensaje esperando en el registro de la función.

La sesión es del navegador. Una función o un servidor solo la tiene si la petición la lleva y el código se la pasa a su cliente.

Cómo saber cuál es tu caso

Mira dónde aparece el mensaje, y luego qué envió de verdad el navegador.

En la consola del navegador, en una página que cualquiera puede abrir. Caso

  1. Abre las herramientas de desarrollo de tu navegador (F12), elige Application, luego Local Storage y la dirección de tu aplicación. supabase-js guarda la sesión bajo una clave formada por sb-, el identificador de tu proyecto y -auth-token. Si no hay ninguna entrada así, este navegador no tiene sesión, y el mensaje dice la verdad. Una aplicación Next.js que usa las herramientas de servidor de Supabase guarda la sesión en cookies, así que busca el mismo nombre en Cookies.

En una página para restablecer la contraseña o de enlace mágico. Caso 2. Mira la dirección en la que se abrió el enlace. Si no es tu aplicación, o es un enlace que tú o un escáner de correo ya habíais abierto, la sesión no tuvo ninguna oportunidad de llegar.

En el registro de una Edge Function, o de tu servidor. Caso 3. Abre la pestaña Network, repite la acción y haz clic en la petición a tu función. Si su cabecera Authorization lleva una cadena larga que empieza por eyJ, el navegador envió el token de una persona, y tu función no se lo pasó a su cliente.

Lo que no encontrarás por ninguna vía es una petición a /auth/v1/user que volviera con este mensaje. Si ves una que falló, es otro error. Una respuesta que dice invalid claim: missing sub claim significa que la clave de tu proyecto se envió donde debía ir una persona, y ese artículo es su gemelo.

El arreglo para cada caso

Dale a cada uno la sesión que le falta, o una respuesta que no la necesite.

Caso 1, el visitante sin sesión: comprueba user antes de usarlo, como en el código de arriba, y muestra la pantalla pensada para alguien que no ha iniciado sesión.

Caso 2, la página para restablecer la contraseña: espera a que el enlace haya iniciado la sesión de la persona antes de mostrar el formulario. Supabase emite un evento PASSWORD_RECOVERY cuando alguien llega desde un enlace de recuperación, y su referencia muestra la página esperándolo:

supabase.auth.onAuthStateChange((event) => {
  if (event === 'PASSWORD_RECOVERY') {
    // el enlace inició su sesión: ahora mostrar el formulario de contraseña nueva
  }
})

Si el evento no llega nunca, el enlace no funcionó. Dilo y ofrece enviar uno nuevo, en lugar de mostrar un formulario que va a fallar con seguridad.

Caso 3, la función o el servidor: pásale la sesión del visitante al cliente que creas. En una Edge Function, créalo con la cabecera que llega:

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()
if (!user) {
  return new Response(JSON.stringify({ error: 'Sign in first' }), {
    status: 401,
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
}

El envoltorio más reciente de Supabase, withSupabase({ auth: 'user' }), hace lo mismo antes de que se ejecute tu manejador, y el artículo sobre Edge Functions muestra cómo montarlo. Mantén el 401 para quien llame sin sesión, para que una petición desde una página sin sesión reciba un rechazo claro. Con la cabecera pasada, getUser() puede devolver ahí invalid claim: missing sub claim para quien llame sin sesión: el mismo visitante visto desde el otro lado, y el 401 cubre los dos.

En una aplicación Next.js, crea el cliente de servidor con la herramienta @supabase/ssr de Supabase y las cookies de la petición, como muestra su configuración de servidor, y llama a getUser() en ese cliente.

Dos arreglos que lo empeoran

Los dos se sugieren para este mensaje, y cada uno para el error quitando la comprobación que hay detrás.

Cambiar getUser() por getSession(). En el navegador no cambia nada que importe: el almacenamiento sigue vacío, así que no obtienes ni sesión ni usuario, solo que sin el mensaje. En un servidor es peligroso. La guía de servidor de Supabase dice que nunca hay que fiarse de supabase.auth.getSession() en código de servidor, porque lee la sesión de la cookie sin volver a validarla. Una cookie es algo que te manda el visitante, y cualquiera puede mandarte la cookie que quiera. getUser() es el método del que supabase-js documenta que hace una petición al servidor de Supabase Auth, así que su resultado es auténtico. Mantenlo en todos los sitios donde la respuesta decide qué puede hacer alguien.

Poner la clave secreta en el cliente para que la llamada deje de fallar. Un cliente creado con la clave service_role o sb_secret_ no necesita sesión para leer y escribir, porque se salta por completo las reglas de tu base de datos. La función vuelve a funcionar, para cualquiera que la llame, con sesión o sin ella. Si un builder hace ese cambio en código que va al navegador, la clave viaja con él a cada visitante. Si ya ha pasado, rótala primero.

Eso se puede comprobar desde fuera. Nuestro escaneo gratuito lee el JavaScript de tu aplicación publicada como lo haría un desconocido y nombra cada clave que encuentra, incluido si es una de las que deben estar ahí. Tarda unos 20 segundos y no pide cuenta: escanea tu app.

Que la comprobación siga ahí tras el próximo cambio

La comprobación de quién ha iniciado sesión es la parte de una página que más fácil desaparece cuando le pides a tu builder que haga desaparecer un error, y los dos arreglos rápidos de arriba la quitan.

Reeve Monitor vuelve a escanear tu app publicada cada hora:

  • las nueve comprobaciones cada hora, en hasta tres apps
  • un correo el día en que un nuevo escaneo califica tu app peor que el anterior; una clave secreta en tu JavaScript limita por sí sola la nota a D
  • si la app está disponible, cada 60 segundos
  • un informe mensual de lo que vio

Un servidor que se cree cualquier cookie, o una función que funciona con la clave secreta, deja que un desconocido cambie las filas de tus usuarios. Reeve Care guarda una copia de tu base de datos de Supabase de antes de que eso pase.

  • 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 en un clic que guarda lo que hay ahora antes de reemplazarlo
  • también tus archivos subidos, en cuanto conectes una credencial de Storage
  • todo lo que hace Monitor

Qué hacer ahora

Qué hacer

  • Lee "Auth session missing!" como "aquí nadie ha iniciado sesión". La petición nunca salió de tu aplicación.
  • Comprueba si user es null antes de usarlo, y dale a un visitante sin sesión la pantalla pensada para él en lugar del error.
  • En una página para restablecer la contraseña, espera al evento PASSWORD_RECOVERY antes de mostrar el formulario, y ofrece un enlace nuevo si nunca llega.
  • En una Edge Function o en un servidor, pásale la cabecera Authorization o las cookies del visitante al cliente que creas, y responde 401 si sigue sin haber usuario.
  • Mantén getUser() en el servidor. Allí getSession() se cree cualquier cookie que le manden.

Si tus sesiones existen y luego desaparecen, de modo que gente que había iniciado sesión se ve fuera de repente, es otro problema con sus propias causas, y por qué se cierra la sesión de los usuarios de Supabase lo cubre. La lista de seguridad de 10 minutos cubre lo que una aplicación recién lanzada suele dejar abierto además.

Preguntas frecuentes

¿Qué significa "Auth session missing!" en Supabase?

Significa que a supabase-js, la biblioteca de Supabase dentro de tu aplicación, le pidieron el usuario con sesión iniciada y no tenía ninguna sesión con la que preguntar. Una sesión es lo que Supabase Auth le da a una persona cuando inicia sesión, y la biblioteca la guarda en el navegador. Si no hay ninguna, no contacta con Supabase en absoluto. Devuelve AuthSessionMissingError con el estado 400 y un usuario null.

¿Por qué veo "Auth session missing!" cuando nadie ha iniciado sesión?

Porque esa es la respuesta para un visitante que no ha iniciado sesión. Tu aplicación llamó a getUser() en una página que cualquiera puede abrir, y supabase-js informó de que nadie había iniciado sesión. No hay nada roto. Tu código solo tiene que leer el usuario como null y mostrar la pantalla de acceso, en lugar de mostrar el error.

¿Por qué mi página para restablecer la contraseña dice "Auth session missing!"?

Cambiar una contraseña con updateUser() necesita una sesión, y en esa página la sesión tiene que venir del enlace del correo. Si el enlace ya se había usado o había caducado, se abrió en otro navegador distinto del que lo pidió, o mandó a la persona a otra dirección, no llegó ninguna sesión y updateUser() no tiene a quién cambiar. Espera al evento PASSWORD_RECOVERY antes de mostrar el formulario, y ofrece un enlace nuevo si nunca llega.

¿Es lo mismo que "invalid claim: missing sub claim"?

No. "Auth session missing!" se decide dentro de tu aplicación, antes de que salga ninguna petición. "invalid claim: missing sub claim" vuelve de Supabase Auth, que recibió una petición y encontró la clave de tu proyecto donde debía estar el token de una persona. Lo primero significa que no había nada que enviar. Lo segundo, que se envió lo que no era.

¿Debo usar getSession() en lugar de getUser()?

No para quitar este error. En el navegador, getSession() encuentra el mismo almacenamiento vacío y no devuelve ninguna sesión, así que nada cambia salvo que el mensaje desaparece. En un servidor es peor: supabase-js documenta que getSession() lee la sesión directamente del almacenamiento, por ejemplo de las cookies de la petición, que pueden no ser auténticas, y recomienda getUser() en su lugar porque lo comprueba con Supabase Auth.

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.