Aggiungi l'accesso a un'app Next.js

Questa guida rapida aggiunge l'accesso con KeenAuth a un'app Next.js App Router con l'SDK @keenauth/nextjs. La tua app è il client OAuth e mantiene ogni token sul server; il browser riceve solo un cookie di sessione cifrato e HttpOnly. Aggiungi le route di accesso, proteggi una pagina, chiami un'API protetta con l'access token dell'utente e testi l'app offline. L'App Router è l'unico router supportato dall'SDK. L'SDK è descritto in Next.js SDK.

L'app ha queste pagine e route:

Route Cosa fa
/ Home page pubblica: un link per accedere, oppure il nome dell'utente e un pulsante per disconnettersi
/dashboard Richiede una sessione. Mostra l'utente e i messaggi restituiti dalla tua API
/api/messages Richiede una sessione. Chiama la tua API con l'access token dell'utente
/auth/login, /auth/callback, /auth/logout Serviti dall'SDK

Prima di iniziare

1. Installa

npm install @keenauth/nextjs next@16.4.2 react@19.3.1 react-dom@19.3.1

L'SDK non è ancora pubblicato. Finché non lo è, l'esempio rimanda al sorgente dell'SDK nel suo package.json.

2. Configura

L'app legge la configurazione solo dalle variabili d'ambiente. Inseriscile in .env.local, che Next.js carica, e non committare mai quel file.

Variabile Obbligatoria Significato
KEENAUTH_ISSUER sì L'issuer del tuo ambiente
KEENAUTH_CLIENT_ID sì Il client_id dell'applicazione web
KEENAUTH_CLIENT_KEY una delle due La chiave privata (una JWK privata JSON) per private_key_jwt, consigliata
KEENAUTH_CLIENT_SECRET una delle due Un client secret
APP_BASE_URL sì L'URL pubblico dell'app. L'SDK costruisce da esso il redirect URI e ogni redirect. http solo per localhost, 127.0.0.1 o [::1]
KEENAUTH_COOKIE_SECRET sì Almeno 32 caratteri; cifra il cookie di sessione. Generane uno con openssl rand -base64 32
API_URL sì L'API che l'app chiama, per esempio http://localhost:8080
KEENAUTH_API_AUDIENCE sì L'identifier di quell'API in KeenAuth

loadConfig legge e controlla questi valori. Si ferma se ne manca uno o se sono impostate entrambe o nessuna delle credenziali del client, e i suoi messaggi di errore non contengono mai un secret.

export function loadConfig(env: Record<string, string | undefined>): AppConfig {
  const need = (name: string): string => {
    const value = env[name];
    if (!value) throw new Error(`${name} is required`);
    return value;
  };

3. Crea l'istanza dell'SDK

createAuth crea l'istanza dell'SDK, e l'SDK controlla ogni opzione al momento della creazione. getApp crea un'istanza per processo al primo utilizzo, così che next build non richiede secret.

/** Builds the SDK instance. The options are checked here, so a configuration error stops the app at start-up. */
export function createAuth(config: AppConfig, overrides: Partial<KeenAuthOptions> = {}): KeenAuth {
  return createKeenAuth({ ...config.auth, ...overrides });
}

let instance: { auth: KeenAuth; config: AppConfig } | undefined;

/** One instance per process, created on first use so that `next build` needs no secrets. */
export function getApp(): { auth: KeenAuth; config: AppConfig } {
  if (!instance) {
    const config = loadConfig(process.env);
    instance = { auth: createAuth(config), config };
  }
  return instance;
}

La configurazione imposta resource con l'identifier della tua API (RFC 8707). Senza, l'access token non è destinato alla tua API e l'API lo rifiuta.

4. Aggiungi le route di accesso

Un unico route handler catch-all serve /auth/login, /auth/callback e /auth/logout:

// Serves /auth/login, /auth/callback and /auth/logout.
export const GET = (request: Request) => getAuth().handlers.GET(request);
export const POST = (request: Request) => getAuth().handlers.POST(request);

L'accesso usa il flusso authorization code con PKCE e una richiesta di autorizzazione pushed quando il tuo ambiente la offre. Il callback verifica state, iss e l'ID token. Un returnTo che non è un percorso della tua app viene ignorato.

5. Proteggi le pagine con il proxy

proxy.ts (middleware.ts prima di Next.js 16) aggiorna la sessione prima che una pagina venga renderizzata e protegge i percorsi che scegli. Una richiesta di pagina senza sessione viene reindirizzata a /auth/login con returnTo; una richiesta a /api/… riceve 401.

export async function proxy(request: NextRequest) {
  const result = await getAuth().proxy(request, { protect: isProtected });
  if (result.response) return result.response;
  const response = NextResponse.next({ request: { headers: result.requestHeaders } });
  result.apply(response);
  return response;
}

I percorsi che richiedono una sessione sono in un'unica funzione:

export function isProtected(pathname: string): boolean {
  return pathname === "/dashboard" || pathname.startsWith("/dashboard/") || pathname.startsWith("/api/");
}

Un matcher del proxy può non coprire un percorso, quindi ogni pagina e ogni route handler che richiede un utente verifica anche la sessione da sé.

6. Chiama la tua API

In un Server Component, leggi la sessione e prendi l'access token dal cookie store. Il token resta sul server; il browser riceve solo l'HTML.

  // The proxy has already sent users without a session to the sign-in; check again here, because a matcher can miss a path.
  const session = await auth.getSession(cookieStore);
  if (!session) return <a href="/auth/login?returnTo=/dashboard">Sign in</a>;

  // The access token stays on the server. The browser receives only the HTML below.
  const response = await fetchMessages(config.apiUrl, await auth.getAccessToken(cookieStore));

session.user contiene i claim dell'ID token, come subject, email e name. La sessione non contiene mai un token.

La chiamata vera e propria è un semplice fetch con un bearer token:

export async function fetchMessages(apiUrl: string, accessToken: string, fetchImpl: typeof fetch = fetch): Promise<Response> {
  return fetchImpl(new URL("/api/messages", apiUrl), {
    headers: { Authorization: `Bearer ${accessToken}` },
    cache: "no-store",
  });
}

Un route handler può anche impostare cookie, quindi usa updateSession, che aggiorna un access token in scadenza:

  // A route handler can set cookies, so updateSession also refreshes an access token that is about to expire.
  const { accessToken, apply } = await auth.updateSession(request);
  if (!accessToken) return new Response(null, { status: 401 });

Non passare l'access token a un Client Component.

7. Disconnettiti

La disconnessione è un form che invia una POST a /auth/logout. L'SDK elimina il cookie di sessione, revoca il refresh token e invia il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app. Una richiesta che il browser contrassegna come proveniente da un altro sito viene rifiutata.

      <form action="/auth/logout" method="post">
        <button type="submit">Sign out</button>
      </form>

8. Avvia l'app

Avvia l'API della guida rapida Express (o qualsiasi API che accetta i token) ed esegui l'app:

npm run dev

Apri http://localhost:3000, scegli Sign in, accedi su KeenAuth e apri la dashboard.

9. Testa senza rete

I test non richiedono rete né un account KeenAuth. Un fetch finto simula il server di autorizzazione (discovery e richiesta di autorizzazione pushed), e Node esegue direttamente i test TypeScript.

test("proxy: a page request without a session is redirected to sign-in with returnTo", async () => {
  const { auth } = app();
  const result = await auth.proxy(new Request(`${APP}/dashboard?tab=1`), { protect: isProtected });
  assert.equal(result.response?.status, 307);
npm test
npm run deps    # pinned versions, lockfile sources and licences

I test verificano che la configurazione sia validata all'avvio, che una pagina senza sessione reindirizzi all'accesso e un percorso API risponda 401, che l'accesso invii una richiesta pushed PKCE S256 e imposti un cookie HttpOnly cifrato, che un callback con uno state sconosciuto venga rifiutato, che una disconnessione da un altro sito venga rifiutata e che l'access token arrivi all'API nell'header Authorization.

L'accesso completo con un browser reale richiede un ambiente KeenAuth. Non fa parte dei test offline.

Dietro un proxy o un load balancer

Imposta APP_BASE_URL con l'URL pubblico dell'app. L'SDK non legge mai Host o X-Forwarded-* per costruire i redirect. Con più istanze del server, un refresh token può essere aggiornato da due di loro contemporaneamente; vedi Limiti attuali.

Prossimi passi