Ir al contenido

Autenticación

El módulo client.auth gestiona la autenticación de usuarios, la administración de tokens y la persistencia de sesiones. Una vez que un usuario inicia sesión, todas las solicitudes de datos posteriores incluyen automáticamente el JWT.

El SDK persiste las sesiones en localStorage de forma predeterminada y actualiza automáticamente los tokens antes de que expiren.

const { user, accessToken, refreshToken } = await client.auth.signInWithEmail(
"user@example.com",
"password"
);
console.log(user.uid, user.email);
const { user, confirmationRequired } = await client.auth.signUp(
"user@example.com",
"password",
"Jane Doe" // optional displayName
);

Con requireEmailVerification activado, no hay sesión hasta que se confirma la dirección: confirmationRequired es true y user es null, así que indícale a la persona que abra el correo. En caso contrario, confirmationRequired es false y user es la cuenta que inició sesión. En 0.23 signUp resuelve { user, accessToken, refreshToken } y siempre inicia sesión.

El SDK incluye métodos dedicados para proveedores OAuth populares, además de un signInWithOAuth() genérico para cualquier proveedor personalizado.

Admite tres estilos de invocación:

// ID-token flow (One Tap / Sign In With Google button)
await client.auth.signInWithGoogle({ idToken: googleIdToken });
// Access-token flow (popup)
await client.auth.signInWithGoogle({ accessToken: googleAccessToken });
// Authorization code flow (most secure, server-side exchange)
await client.auth.signInWithGoogle({ code: authCode, redirectUri: "https://..." });

Cada proveedor sigue el flujo de código de autorización con (code, redirectUri):

await client.auth.signInWithGitHub(code, redirectUri);
await client.auth.signInWithMicrosoft(code, redirectUri);
await client.auth.signInWithFacebook(code, redirectUri);
await client.auth.signInWithLinkedin(code, redirectUri);
await client.auth.signInWithDiscord(code, redirectUri);
await client.auth.signInWithGitLab(code, redirectUri);
await client.auth.signInWithBitbucket(code, redirectUri);
await client.auth.signInWithSlack(code, redirectUri);
await client.auth.signInWithSpotify(code, redirectUri);

Apple y Twitter requieren parámetros adicionales:

// Apple — optional user info from first sign-in
await client.auth.signInWithApple(code, redirectUri, {
name: { firstName: "Jane", lastName: "Doe" },
email: "jane@example.com"
});
// Twitter — requires PKCE code verifier
await client.auth.signInWithTwitter(code, redirectUri, codeVerifier);

Para cualquier proveedor registrado en el backend:

await client.auth.signInWithOAuth("custom-provider", {
code: authCode,
redirectUri: "https://myapp.com/callback"
});

Un enlace de inicio de sesión de un solo clic por correo electrónico. El enlace siempre es <base>/auth/magic-link?token=…, donde <base> es el email.magicLinkUrl del backend, o su base de restablecimiento de contraseña (FRONTEND_URL en el runtime) cuando eso no está establecido. No hay un redirectTo por solicitud. Sirve esa ruta en tu frontend, y devuelve el token para canjearlo por una sesión. El CMS lo sirve, iniciando sesión (a través del paso de código cuando hay un segundo factor); otros frontends necesitan una página de aterrizaje.

// 1. Ask for the link.
await client.auth.sendMagicLink("user@example.com");
// 2. On the landing page, trade the token for a session.
const token = new URLSearchParams(location.search).get("token")!;
const { user } = await client.auth.verifyMagicLink(token);

sendMagicLink responde lo mismo independientemente de si la dirección tiene o no una cuenta. Esto es deliberado: un endpoint que respondiera “no existe dicho usuario” sería un oráculo de enumeración de cuentas, así que no uses el resultado para decirle a una persona si está registrada — no lo sabe.

Ambos necesitan un servicio de correo electrónico configurado en el backend, o responderán 503 EMAIL_NOT_CONFIGURED.

Un código de seis dígitos por correo electrónico, para los casos en los que un enlace resulta incómodo: una aplicación nativa, un segundo dispositivo o un navegador que altere los enlaces.

const { expiresInSeconds } = await client.auth.sendEmailOtp("user@example.com");
// The address goes back with the code, because the code is only valid for it.
const { user } = await client.auth.verifyEmailOtp("user@example.com", "418293");

Enviar la dirección junto con el código es lo que hace que un intento de adivinar los seis dígitos sea contra una sola cuenta en lugar de contra todas las cuentas a la vez.

Inicia la sesión de un visitante sin ninguna credencial, para que pueda comenzar a usar la aplicación antes de tener un motivo para registrarse:

const { user } = await client.auth.signInAnonymously();
user.isAnonymous; // true

La cuenta es real: tiene un id, roles y una sesión, por lo que la seguridad a nivel de fila (row-level security) delimita sus filas exactamente como lo haría con las de un usuario registrado. Lo que no tiene es una forma de volver: nadie puede iniciar sesión como esa cuenta por segunda vez, por lo que todo lo que le pertenece se pierde con la sesión.

linkAnonymous es la forma en que deja de ser desechable. El usuario conserva su id, por lo que todo lo que creó mientras era anónimo sigue siendo suyo:

await client.auth.linkAnonymous("user@example.com", "correct-horse-battery");
Fallo Significado
ANONYMOUS_AUTH_DISABLED (403) El backend no ha habilitado la autenticación anónima
NOT_ANONYMOUS (400) La sesión actual pertenece a una cuenta ordinaria
EMAIL_EXISTS (409) La dirección ya tiene una cuenta — inicia sesión en esa en su lugar

Vincular un proveedor a una cuenta existente

Sección titulada «Vincular un proveedor a una cuenta existente»

signInWithGoogle y métodos similares inician la sesión de un usuario. linkProvider vincula una identidad de proveedor a la cuenta que ya ha iniciado sesión, para que la misma persona pueda volver a través de cualquiera de las dos vías:

await client.auth.linkProvider("google", { idToken });

La sesión ya demuestra la propiedad de la cuenta, por lo que, a diferencia del inicio de sesión, esto no requiere que el proveedor haya verificado el correo electrónico y las dos direcciones no necesitan coincidir. Tiene éxito de forma idempotente (alreadyLinked: true) cuando esa identidad ya está en esta cuenta, y rechaza con IDENTITY_ALREADY_LINKED (409) cuando pertenece a una diferente.

const profile = await client.auth.findUserByEmail("user@example.com");
// { uid, displayName, photoURL } | null

Tres campos no sensibles y nada más — lo suficiente para mostrar “estás invitando a Jane” antes de enviar una invitación.

Factores TOTP — una aplicación de autenticación — más el desafío que eleva una sesión de aal1 a aal2.

const { factor, totp, recoveryCodes } = await client.auth.mfa.enroll({
friendlyName: "Phone"
});
showQrCode(totp.uri); // otpauth://… — what the authenticator scans
if (recoveryCodes) showRecoveryCodes(recoveryCodes);

Muestra los códigos de recuperación una sola vez y nunca más. Solo se almacenan sus hashes, por lo que nada podrá mostrarlos más tarde. Vienen con el primer factor de la cuenta. Añadir otro factor conserva los códigos que la cuenta ya tiene, y recoveryCodes es null. Iniciar un registro y abandonarlo nunca los toca.

Para reemplazarlos, después de usar varios o perder la copia impresa, llama a regenerateRecoveryCodes() desde una sesión aal2. Los códigos antiguos dejan de funcionar:

const { recoveryCodes } = await client.auth.mfa.regenerateRecoveryCodes();

A quien haya perdido tanto el autenticador como los códigos, un administrador puede dejarlo entrar de nuevo: client.admin.resetMfa(uid) (DELETE /api/admin/users/:uid/mfa, users:write) elimina los factores y códigos de la cuenta y termina sus sesiones, de modo que la contraseña vuelve a iniciar sesión sin un segundo factor. En 0.23 cada registro devuelve un nuevo conjunto de códigos que sustituye al anterior, y no existen ni regenerateRecoveryCodes() ni resetMfa.

El factor no se puede utilizar hasta que el usuario demuestre que su aplicación de autenticación produjo un código a partir de ese secreto:

await client.auth.mfa.verify(factor.id, "418293");

Un inicio de sesión contra una cuenta con MFA registrado no devuelve ninguna sesión. Se rechaza con 401 MFA_REQUIRED, y los details del error incluyen un mfaToken y los factors verificados de la cuenta. Pasa ese token a challenge y verifyChallenge para obtener la sesión:

import { RebaseApiError } from "@rebasepro/client";
type MfaRequired = {
mfaToken: string;
factors: { id: string; factorType: string; friendlyName?: string }[];
};
try {
await client.auth.signInWithEmail(email, password);
} catch (e) {
if (!(e instanceof RebaseApiError) || e.code !== "MFA_REQUIRED") throw e;
const { mfaToken, factors } = e.details as MfaRequired;
const { challengeId } = await client.auth.mfa.challenge(factors[0].id, { mfaToken });
// A TOTP code, or one of the recovery codes.
const { user } = await client.auth.mfa.verifyChallenge(challengeId, "418293", { mfaToken });
}

El mfaToken se envía solo en esas dos peticiones y nunca se instala en el cliente. verifyChallenge genera la sesión aal2, este cliente la adopta y emite SIGNED_IN como en cualquier otro inicio de sesión. El mfaToken expira cinco minutos después del inicio de sesión que lo devolvió, y un desafío cinco minutos después de abrirse. Un desafío que ha alcanzado su límite de intentos permanece inhabilitado durante el resto de su vida útil; de lo contrario, un desafío abierto permitiría intentos ilimitados para adivinar los seis dígitos.

Sin mfaToken, las dos llamadas elevan la sesión que este cliente ya tiene de aal1 a aal2.

await client.auth.mfa.unenroll(factorId);

Requiere una sesión aal2 — una que ya haya respondido a un desafío —, de modo que un token aal1 robado no pueda desactivar MFA. Eliminar el último factor verificado también descarta los códigos de recuperación.

await client.auth.signOut();

Esto revoca el token de actualización en el servidor, borra la sesión local y emite un evento SIGNED_OUT.

const session = client.auth.getSession();
// { accessToken, refreshToken, expiresAt, user } | null

Obtener el usuario actual (verificado por el servidor)

Sección titulada «Obtener el usuario actual (verificado por el servidor)»
const user = await client.auth.getUser();
// Fetches the user from the backend (GET /auth/me)
const updatedUser = await client.auth.updateUser({
displayName: "Jane Doe",
photoURL: "https://example.com/avatar.jpg"
});

La actualización del token se realiza automáticamente, pero puedes activarla manualmente:

const session = await client.auth.refreshSession();
const client = createRebaseClient({
baseUrl: API_URL,
auth: { authFlowMode: "cookie" }
});
Modo Dónde está el refresh token Cuándo usarlo
"json" (predeterminado) Devuelto en el cuerpo de la respuesta, conservado en localStorage Una aplicación nativa, un script, cualquier cosa sin el almacén de cookies de un navegador
"cookie" Una cookie HttpOnly establecida por el backend Una aplicación de navegador. Los scripts que se ejecutan en tu página no pueden leerla, lo que la hace segura contra ataques XSS

El modo cookie necesita auth.cookieAuth en el backend, y es el que utiliza la plantilla de frontend generada.

Una sesión restaurada no está disponible en el primer renderizado. getSession() es síncrono, por lo que al cargar la página devuelve null mientras la restauración aún está en curso — y en el modo cookie una restauración siempre está en curso, porque el token de actualización está en una cookie que la página no puede leer, por lo que el cliente tiene que pedirle al servidor un nuevo token de acceso.

Leerlo sincrónicamente es lo que produce un parpadeo de sesión cerrada en cada recarga:

// Wrong: renders the signed-out view for one round trip, every reload.
const session = client.auth.getSession();
if (!session) return <SignIn />;

isInitialized() se resuelve una vez que el cliente ha terminado de intentarlo, haya encontrado una sesión o no:

async function currentUser() {
await client.auth.isInitialized();
return client.auth.getSession()?.user ?? null;
}

En React, esto es un efecto:

import { useEffect, useState } from "react";
function useCurrentUser() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
client.auth.isInitialized().then(() => {
if (cancelled) return;
setUser(client.auth.getSession()?.user ?? null);
setLoading(false);
});
return () => { cancelled = true; };
}, []);
return { user, loading };
}

useRebaseAuthController en @rebasepro/app ya hace esto, por lo que una aplicación construida sobre la plantilla generada lo obtiene de forma automática.

Una restauración exitosa también llega a onAuthStateChange como TOKEN_REFRESHED — es una actualización —, pero un escuchador por sí solo no puede indicarte que la restauración ha finalizado: un arranque sin sesión no emite absolutamente nada, lo cual es indistinguible de uno que aún está en curso. Espera con await isInitialized() para esa comprobación y utiliza el escuchador para los cambios posteriores.

Reacciona a los cambios de autenticación en toda tu aplicación:

const unsubscribe = client.auth.onAuthStateChange((event, session) => {
// event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
console.log("Auth event:", event);
console.log("Session:", session?.user?.email);
});
// Stop listening
unsubscribe();
Evento Cuándo
SIGNED_IN Se completó un inicio de sesión o registro
TOKEN_REFRESHED El token de acceso fue renovado — incluida la renovación silenciosa que restaura una sesión al cargar la página
USER_UPDATED updateUser() modificó el perfil, o confirmEmailChange() trasladó esta cuenta a su nueva dirección
SIGNED_OUT Un cierre de sesión, o una actualización que falló definitivamente
const { success, message } = await client.auth.resetPasswordForEmail(
"user@example.com"
);
const { success, message } = await client.auth.resetPassword(
resetToken,
"newSecurePassword"
);
const { success, message } = await client.auth.changePassword(
"oldPassword",
"newPassword"
);

Toda otra sesión de la cuenta termina: quien tuviera otra tiene que iniciar sesión de nuevo con la nueva contraseña. Este dispositivo permanece conectado. El servidor responde con una sesión nueva, y el cliente la adopta (emitiendo TOKEN_REFRESHED).

El registro envía por correo a la nueva cuenta su enlace de verificación cuando el correo electrónico está configurado. sendVerificationEmail() lo envía de nuevo.

// Send verification email to the current user
await client.auth.sendVerificationEmail();
// Verify with the token from the email link. Signed in as that account, this
// keeps everything on it.
const { passwordRemoved } = await client.auth.verifyEmail(token);
// Signed out, an account that holds a password answers PROOF_REQUIRED:
try {
await client.auth.verifyEmail(token);
} catch (e) {
if (e instanceof Error && "code" in e && e.code === "PROOF_REQUIRED") {
// Keep the password, and sign in:
await client.auth.verifyEmail(token, { password });
// …or verify without it, which removes it:
// await client.auth.verifyEmail(token, { removeUnproven: true });
}
}

El enlace demuestra el acceso a la bandeja de entrada, no quién registró la dirección, por lo que una contraseña que ni la sesión ni la llamada demuestran se elimina en lugar de conservarse — consulta Verificación de correo electrónico.

En un backend con requireEmailVerification, signUp() se resuelve con { confirmationRequired: true, user: null } y sin sesión: la cuenta inicia sesión una vez que se sigue el enlace con su contraseña.

Cambiar la dirección de correo electrónico

Sección titulada «Cambiar la dirección de correo electrónico»

Un usuario que ha iniciado sesión traslada su propia cuenta a otra dirección. Nada cambia hasta que la nueva dirección responde:

// Mails a link to the new address, and a notice to the current one
const { pendingEmail, expiresAt } = await client.auth.changeEmail("jane@new.example");
// On the page the link opens (<frontend>/confirm-email-change?token=…):
const { email, removedProviders } = await client.auth.confirmEmailChange(token);

El enlace vive 24 horas; getUser() informa la dirección en espera como pendingEmail. Confirmar no necesita sesión, y un cliente que ha iniciado sesión como esa cuenta adopta la nueva dirección y emite USER_UPDATED. Los rechazos se enumeran en Cambiar una dirección de correo electrónico.

// List all active sessions, one per sign-in
const sessions = await client.auth.getSessions();
// [{ id, userAgent, ipAddress, createdAt, isCurrentSession }, …]
// Revoke a specific session
await client.auth.revokeSession(sessionId);
// Revoke ALL sessions (logs out everywhere)
await client.auth.revokeAllSessions();

Cada token de acceso nombra la sesión a la que pertenece, de modo que exactamente una entrada tiene isCurrentSession: true: el dispositivo que pregunta. Revocar una sesión termina el refresh token de ese dispositivo y su token de acceso juntos, a partir de la siguiente solicitud, tanto por HTTP como por un socket de tiempo real abierto. Cerrar sesión (signOut()) hace lo mismo para el dispositivo que cierra sesión. Un token de acceso emitido antes de esta versión no nombra ninguna sesión; sigue funcionando hasta que expira, dentro de la hora.

Consulta la configuración de autenticación del backend:

const config = await client.auth.getAuthConfig();
// {
// hasBuiltInAuthRoutes: boolean,
// emailPasswordLogin: boolean,
// registrationEnabled: boolean, // open right now, bootstrap window included
// passwordReset: boolean, // needs an email service
// adminPasswordReset: boolean,
// sessionManagement: boolean,
// profileUpdate: boolean,
// emailVerification: boolean,
// magicLink: boolean,
// anonymousLogin: boolean,
// enabledProviders: string[],
// needsSetup: boolean
// }

De forma predeterminada, las sesiones se almacenan en localStorage. Puedes personalizar esto con la opción auth:

import { createRebaseClient, createCookieStorage } from "@rebasepro/client";
// Use cookies instead of localStorage
const client = createRebaseClient({
baseUrl: import.meta.env.VITE_API_URL,
auth: {
storage: createCookieStorage({
path: "/",
sameSite: "Lax",
secure: true
}),
autoRefresh: true, // default: true
persistSession: true // default: true
}
});
// Canonical type — import from @rebasepro/types
interface User {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
providerId: string;
isAnonymous: boolean;
emailVerified?: boolean;
pendingEmail?: string | null; // an address change waiting for its link
roles?: string[]; // text[] from the users table
metadata?: Record<string, unknown>;
}