Salta ai contenuti

Autenticazione

Il modulo client.auth gestisce l’autenticazione degli utenti, la gestione dei token e la persistenza della sessione. Una volta effettuato l’accesso, tutte le richieste di dati successive includono automaticamente il JWT.

L’SDK memorizza le sessioni in localStorage per impostazione predefinita e aggiorna automaticamente i token prima della loro scadenza.

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 abilitato, non c’è alcuna sessione finché l’indirizzo non viene confermato: confirmationRequired è true e user è null, quindi invita l’utente ad aprire l’email. Altrimenti confirmationRequired è false e user è l’account autenticato. Nella versione 0.23 signUp risolve { user, accessToken, refreshToken } ed effettua sempre l’accesso.

L’SDK include metodi dedicati per i provider OAuth più diffusi, oltre a un generico signInWithOAuth() per qualsiasi provider personalizzato.

Supporta tre modalità di chiamata:

// 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://..." });

Ciascun provider segue il flusso del codice di autorizzazione 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);

Twitter richiede anche il code verifier PKCE:

await client.auth.signInWithTwitter(code, redirectUri, codeVerifier);

In un browser, signInWithApplePopup() apre il popup di Apple, controlla lo state ed esegue l’accesso con il codice. Chiamalo da un gestore di click. Chiama preloadAppleSignIn() al montaggio, poiché Safari blocca i popup in attesa del download di uno script:

import { preloadAppleSignIn } from "@rebasepro/client";
void preloadAppleSignIn(); // on mount
await client.auth.signInWithApplePopup({ clientId: "com.example.web" }); // on click

Senza clientId, l’SDK legge il Services ID da /auth/config, il che ritarda il popup. L’URI di reindirizzamento predefinito è l’origine della pagina seguita da /, e deve essere un Return URL sul Services ID (HTTPS, mai localhost). La chiusura del popup genera un rifiuto con il codice APPLE_SIGN_IN_CANCELLED.

Un’app nativa iOS o macOS invia il codice di autorizzazione proveniente dall’accesso nativo di Apple con il relativo bundle ID. Il bundle ID deve essere presente in APPLE_BUNDLE_IDS:

await client.auth.signInWithApple({
code: credential.authorizationCode,
bundleId: "com.example.ios",
user: { name: { firstName: "Jane" } } // sent by Apple on the first sign-in only
});

Per qualsiasi provider registrato sul backend:

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

Un link di accesso con un clic inviato via email. Il link è sempre <base>/auth/magic-link?token=…, dove <base> corrisponde a email.magicLinkUrl del backend, oppure alla sua base per il ripristino della password (FRONTEND_URL nel runtime) quando non è impostato. Non è previsto un parametro redirectTo per singola richiesta. Gestisci tale percorso nel tuo frontend e restituisci il token per scambiarlo con una sessione. Il CMS lo gestisce automaticamente, effettuando l’accesso (tramite il passaggio del codice in presenza di un secondo fattore); gli altri frontend necessitano di una landing page.

// 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 risponde nello stesso modo indipendentemente dal fatto che l’indirizzo appartenga o meno a un account. È una scelta deliberata: un endpoint che rispondesse “utente inesistente” fungerebbe da oracolo per l’enumerazione degli account, quindi non utilizzare il risultato per comunicare a una persona se è registrata o meno — l’endpoint non lo sa.

L’invio richiede la configurazione di un servizio email sul backend, altrimenti sendMagicLink risponderà con l’errore 503 EMAIL_NOT_CONFIGURED.

Un codice a sei cifre inviato via email, ideale per le situazioni in cui un link risulterebbe poco pratico: un’app nativa, un secondo dispositivo, un browser che altera i link.

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");

L’invio dell’indirizzo insieme al codice è ciò che limita un tentativo di indovinare le sei cifre a un solo account, invece di consentire tentativi simultanei contro tutti gli account.

Fai accedere un visitatore senza alcuna credenziale, consentendogli di iniziare a usare l’app prima ancora di avere un motivo per registrarsi:

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

L’account è reale: ha un ID, dei ruoli e una sessione, quindi la row-level security ne delimita le righe esattamente come farebbe per un utente registrato. Ciò che non possiede è un modo per tornare indietro: nessuno può accedere come tale account una seconda volta, quindi tutto ciò che possiede va perso con la sessione.

linkAnonymous è il modo per evitare che sia usa e getta. L’utente mantiene il proprio ID, quindi tutto ciò che ha creato mentre era anonimo rimane di sua proprietà:

await client.auth.linkAnonymous("user@example.com", "correct-horse-battery");
Errore Significato
ANONYMOUS_AUTH_DISABLED (403) Il backend non ha abilitato l’autenticazione anonima, oppure l’ha disattivata dopo l’accesso di questo ospite: un ospite diventa un account solo finché allowAnonymous è attivo
REGISTRATION_DISABLED (403) La registrazione è chiusa sul backend, e la conversione in un account equivale a una registrazione
NOT_ANONYMOUS (400) La sessione corrente appartiene a un account ordinario
EMAIL_EXISTS (409) L’indirizzo ha già un account associato — accedi a quell’account

Un ospite che non torna più rimane memorizzato come riga nel database. Un amministratore può rimuoverli con client.admin.listGuests(), client.admin.deleteGuest(uid) e client.admin.deleteGuests({ lastSeenBefore }), oppure su Rebase Cloud con rebase cloud guests. Questi comandi agiscono esclusivamente sugli ospiti, e deleteGuests funziona a lotti: ripetilo finché remaining non è 0, impostando offset sul numero di record saltati finora.

Collegamento di un provider a un account esistente

Sezione intitolata “Collegamento di un provider a un account esistente”

signInWithGoogle e gli altri metodi simili eseguono l’accesso di un utente. linkProvider associa l’identità di un provider all’account che ha già effettuato l’accesso, consentendo alla stessa persona di rientrare utilizzando una qualsiasi delle due opzioni:

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

La sessione dimostra già la proprietà dell’account, quindi, a differenza dell’accesso, questa operazione non richiede che il provider abbia verificato l’email, e non è necessario che i due indirizzi coincidano. Ha successo in modo idempotente (alreadyLinked: true) quando tale identità è già associata a questo account, e rifiuta con IDENTITY_ALREADY_LINKED (409) quando appartiene a un altro account.

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

Tre campi non sensibili e nient’altro — quanto basta per mostrare “stai invitando Jane” prima di inviare un invito. Disponibile solo su consenso esplicito: riservato a chiamanti autenticati e restituisce 404 a meno che il backend non imposti AUTH_ALLOW_USER_LOOKUP=true (auth.allowUserLookup).

Registrazione di un fattore, secondo passaggio dell’accesso, codici di recupero e rimozione di un fattore: Autenticazione a più fattori.

await client.auth.signOut();

Questo revoca il refresh token sul server, cancella la sessione locale ed emette un evento SIGNED_OUT.

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

Non conservare mai l’oggetto sessione. getSession() è un’istantanea. Ogni rinnovo dell’access token, all’incirca una volta ogni ora, sostituisce la sessione con un nuovo oggetto; pertanto, una sessione memorizzata al caricamento della pagina contiene un token che il server rifiuterà una volta scaduto, facendo apparire l’app come disconnessa. Leggi la sessione al momento del bisogno.

La chiamata a una funzione in backend/functions/ non richiede alcuna gestione manuale del token: client.functions.invoke rinnova il token prima dell’invio ed effettua un nuovo tentativo in caso di errore 401.

const saved = await client.functions.invoke("progress", { level: 3 }, { path: "save" });

Per qualsiasi altra richiesta, richiedi il token di volta in volta con getAccessToken(). Questo metodo rinnova un token scaduto o prossimo alla scadenza, attende il ripristino della sessione al caricamento e restituisce null quando nessun utente ha effettuato l’accesso.

const token = await client.auth.getAccessToken();
await fetch("https://other.example.com/upload", {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: file
});

Ottenere l’utente corrente (verificato dal server)

Sezione intitolata “Ottenere l’utente corrente (verificato dal server)”
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"
});

L’aggiornamento del token avviene automaticamente, ma puoi avviarlo manualmente:

const session = await client.auth.refreshSession();

Una richiesta non viene mai inviata con un token che il client sa essere scaduto: le richieste condividono un unico aggiornamento e vengono inviate una sola volta, con il nuovo token. Un token rifiutato prematuramente dal server beneficia comunque di un aggiornamento, seguito da un nuovo tentativo per richiesta.

const client = createRebaseClient({
baseUrl: API_URL,
auth: { authFlowMode: "cookie" }
});
Modalità Dove risiede il refresh token Predefinito
"cookie" Un cookie HttpOnly impostato dal backend. Gli script in esecuzione sulla pagina non possono leggerlo, il che lo rende protetto da vulnerabilità XSS Nel browser
"json" Restituito nel corpo della risposta, memorizzato in storage (localStorage per impostazione predefinita) Al di fuori del browser (Node, worker, React Native) e nel browser quando passi un tuo storage personalizzato

Le due parti devono concordare. La modalità cookie richiede auth.cookieAuth sul backend, e il runtime di Rebase, utilizzato da Rebase Cloud, lo attiva sempre. La modalità json richiede un backend privo di tale impostazione: a fronte di un backend con cookieAuth, a un client json non viene fornito alcun refresh token, e la sua sessione termina alla scadenza dell’access token, circa un’ora dopo l’accesso. Quando la risposta a un accesso evidenzia una mancata corrispondenza tra le modalità, il client registra un errore indicando quale modalità utilizzare.

Il passaggio di uno storage personalizzato mantiene un client browser in modalità json. Questa configurazione è utile per un secondo client sulla pagina che gestisce una propria sessione di accesso separata dal cookie della pagina: in modalità cookie non verrebbe memorizzato nulla in tale storage.

Una sessione ripristinata non è disponibile al primo render. getSession() è sincrono, pertanto al caricamento della pagina restituisce null mentre il ripristino è ancora in corso — e in modalità cookie un ripristino è sempre in corso, perché il refresh token si trova in un cookie che la pagina non può leggere, obbligando il client a richiedere al server un nuovo access token.

Leggerlo in modo sincrono è la causa del lampo di stato non autenticato (flash) a ogni ricaricamento:

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

isInitialized() si risolve una volta che il client ha terminato il tentativo — indipendentemente dal fatto che abbia trovato una sessione o meno:

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

In React, questo si traduce in un unico effetto:

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 in @rebasepro/app gestisce già questo comportamento, quindi un’applicazione creata sul template generato ne beneficia automaticamente. Le richieste di dati non devono attendere: una richiesta effettuata durante il ripristino viene inviata non appena questo si stabilizza — come utente ripristinato oppure in modo anonimo — evitando così che un primo render legga un elenco pubblico al posto dei dati riservati dell’utente.

Un ripristino che effettua un aggiornamento (un token memorizzato scaduto o la modalità cookie) raggiunge onAuthStateChange come TOKEN_REFRESHED; un token memorizzato ancora valido viene adottato in modo trasparente. Pertanto, un listener non può confermare l’avvenuto completamento del ripristino: un avvio che non emette alcun evento è indistinguibile da uno ancora in corso. Attendi isInitialized() per verificare questo stato e usa il listener per i cambiamenti successivi.

restoreOutcome() indica come si è concluso il ripristino, una volta che isInitialized() è stato risolto: "restored", "none" (nulla da ripristinare), "refused" (il server ha rifiutato la sessione) o "unreachable" (il server non è stato raggiungibile, quindi la sessione non è stata né confermata né rifiutata). In modalità cookie, "unreachable" in assenza di sessione rappresenta l’unico caso in cui non è certo che lo stato sia effettivamente “disconnesso”:

await client.auth.isInitialized();
if (!client.auth.getSession() && client.auth.restoreOutcome() === "unreachable") {
console.log("Offline: could not check the session.");
}

Reagisci alle variazioni di autenticazione in tutta l’applicazione:

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 Quando
SIGNED_IN È stato completato un accesso o una registrazione
TOKEN_REFRESHED L’access token è stato rinnovato — compreso il rinnovo trasparente che ripristina una sessione al caricamento della pagina
USER_UPDATED updateUser() ha modificato il profilo, linkAnonymous() ha convertito un ospite in un account, changeEmail() ha avviato un cambio di indirizzo oppure confirmEmailChange() ha trasferito questo account al nuovo indirizzo
SIGNED_OUT Una disconnessione, revokeAllSessions() o un aggiornamento del token fallito 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"
);

Tutte le altre sessioni dell’account vengono terminate: chiunque ne possedesse una dovrà effettuare nuovamente l’accesso con la nuova password. Questo dispositivo rimane autenticato. Il server risponde con una nuova sessione e il client la adotta (emettendo TOKEN_REFRESHED).

Al momento della registrazione, al nuovo account viene inviato il link di verifica se il servizio email è configurato. sendVerificationEmail() lo invia nuovamente.

// 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 });
}
}

Il link attesta l’accesso alla casella di posta, non l’identità di chi ha registrato l’indirizzo; pertanto, una password non convalidata né dalla sessione né dalla chiamata viene rimossa anziché conservata — consulta Verifica dell’email.

Su un backend con requireEmailVerification, signUp() restituisce { confirmationRequired: true, user: null } senza alcuna sessione: l’account effettua l’accesso una volta aperto il link inserendo la propria password.

Un utente autenticato trasferisce il proprio account a un altro indirizzo. Nulla cambia finché il nuovo indirizzo non conferma:

// 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);

Il link ha una validità di 24 ore; getUser() segnala l’indirizzo in attesa come pendingEmail. La conferma non richiede alcuna sessione e un client autenticato con quell’account acquisisce il nuovo indirizzo ed emette USER_UPDATED. I motivi di rifiuto sono elencati in Modifica di un indirizzo email.

// 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();

Ogni access token specifica la sessione di accesso a cui appartiene, pertanto esattamente una voce presenterà isCurrentSession: true: quella del dispositivo che effettua la richiesta. La revoca di una sessione termina contemporaneamente il refresh token e l’access token di quel dispositivo a partire dalla richiesta successiva, sia su HTTP che su un socket realtime aperto. La disconnessione (signOut()) esegue la stessa operazione per il dispositivo che si disconnette. Un access token emesso prima di questa versione non specifica alcuna sessione; continuerà a funzionare fino alla sua scadenza, entro un’ora.

Interroga la configurazione di autenticazione 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, emailOtp?: boolean,
// anonymousLogin: boolean, enabledProviders: string[],
// providerClientIds?: Record<string, string>, // public ids, e.g. { apple: Services ID }
// externalLoginUrl?: string, needsSetup?: boolean, storage?: boolean
// }

Per impostazione predefinita, le sessioni sono memorizzate in localStorage. Puoi personalizzare questo comportamento con l’opzione 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
}
});

Clerk, Firebase Auth o una sessione già esistente nell’applicazione, con un getter di token al posto di client.auth: Un proprio provider di autenticazione.

// 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>;
}