
# SDK per Next.js

`@keenauth/nextjs` aggiunge l'accesso a un'applicazione Next.js con App Router. La tua app è il client OAuth e conserva tutti i token sul server. Il browser riceve solo un cookie di sessione cifrato e HttpOnly, e non vede mai un access token, un refresh token o un ID token. Questo è il pattern backend-for-frontend (BFF). Il pacchetto non ha dipendenze di runtime. Non è ancora pubblicato su npm.

## Prima di iniziare

1. Nella dashboard, crea un'applicazione web nel tuo ambiente.
2. Scegli l'autenticazione client `private_key_jwt` e registra la tua chiave pubblica. Funziona anche un client secret.
3. Aggiungi il redirect URI `https://app.acme.example/auth/callback` e il redirect URI post-logout `https://app.acme.example/`.
4. Genera un secret per il cookie di almeno 32 caratteri, per esempio con `openssl rand -base64 32`. Conservalo nel tuo secret store, non nel repository.

## Crea l'istanza

```ts
// lib/auth.ts
import { createKeenAuth } from "@keenauth/nextjs";

export const auth = createKeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "private_key_jwt", privateKey: JSON.parse(process.env.KEENAUTH_CLIENT_KEY!) },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
```

`appBaseUrl` è l'indirizzo pubblico della tua app. L'SDK costruisce da esso il redirect URI e tutti i redirect, mai dagli header `Host` o `X-Forwarded-*` della richiesta. Il protocollo `http` è accettato solo per `localhost`.

Le opzioni vengono verificate al momento della creazione dell'istanza, quindi un errore di configurazione blocca l'app all'avvio.

## Aggiungi le route

```ts
// app/auth/[...keenauth]/route.ts
import { auth } from "@/lib/auth";

export const { GET, POST } = auth.handlers;
```

Questo serve tre route:

| Route | Cosa fa |
|---|---|
| `/auth/login` | Avvia l'accesso. Il parametro di query opzionale `returnTo` imposta dove l'utente arriva dopo. |
| `/auth/callback` | Completa l'accesso e imposta il cookie di sessione. |
| `/auth/logout` | Disconnette l'utente in KeenAuth e riporta alla tua app. |

L'accesso usa il flusso authorization code con PKCE. La richiesta viene inviata al server (PAR) quando il tuo ambiente la supporta. Nel callback l'SDK verifica `state`, `iss` e l'ID token, incluso il suo `nonce`.

`returnTo` deve essere un percorso della tua app, per esempio `/orders?tab=open`. Qualsiasi altro valore, come `//evil.example` o un URL completo, viene ignorato e l'utente arriva su `/`.

## Proteggi le pagine e rinnova i token

```ts
// proxy.ts (middleware.ts before Next.js 16)
import { NextResponse, type NextRequest } from "next/server";
import { auth } from "@/lib/auth";

export async function proxy(request: NextRequest) {
  const result = await auth.proxy(request, {
    protect: (path) => path.startsWith("/orders") || path.startsWith("/api/"),
  });
  if (result.response) return result.response;
  const response = NextResponse.next({ request: { headers: result.requestHeaders } });
  result.apply(response);
  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

Per un percorso protetto senza sessione:

- una richiesta di pagina (`GET` o `HEAD`) viene reindirizzata a `/auth/login` con `returnTo`;
- una richiesta a `/api/…`, o con un altro metodo, riceve `401` con un documento problem.

Quando l'access token scade entro 60 secondi, il proxy lo rinnova. Ogni rinnovo ruota il refresh token e il nuovo cookie di sessione viene impostato nella risposta. `requestHeaders` passa la nuova sessione alle pagine che renderizzano la stessa richiesta.

Se il server rifiuta il refresh token, la sessione termina. Un errore temporaneo del server lascia la sessione invariata.

Verifica inoltre la sessione in ogni Server Function e route handler. Il matcher del proxy può non coprire un percorso.

## Leggi la sessione

In un Server Component, passa il cookie store:

```tsx
// app/orders/page.tsx
import { cookies } from "next/headers";
import { auth } from "@/lib/auth";

export default async function Orders() {
  const session = await auth.getSession(await cookies());
  if (!session) return <a href="/auth/login?returnTo=/orders">Sign in</a>;
  return <p>Signed in as {session.user.email}</p>;
}
```

`session.user` contiene le claim tipizzate dell'ID token: `subject`, `email`, `emailVerified`, `name`, `orgId`, `orgRoles` e altre. Tutte le claim, incluse quelle personalizzate, sono in `session.user.claims`. L'oggetto sessione non contiene mai un token.

Un Server Component non può impostare cookie, quindi leggere la sessione lì non la rinnova mai.

## Chiama la tua API

In un Server Component, prendi l'access token dal cookie store:

```ts
const token = await auth.getAccessToken(await cookies());
const res = await fetch("https://api.acme.example/v1/orders", {
  headers: { Authorization: `Bearer ${token}` },
});
```

`getAccessToken` fallisce con:

- `refresh_required` quando il token è vicino alla scadenza e il proxy avrebbe dovuto rinnovarlo;
- `login_required` quando non c'è una sessione.

In un route handler, anche `updateSession` rinnova:

```ts
// app/api/orders/route.ts
import { auth } from "@/lib/auth";

export async function GET(request: Request) {
  const { accessToken, apply } = await auth.updateSession(request);
  if (!accessToken) return new Response(null, { status: 401 });
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const response = new Response(upstream.body, { status: upstream.status });
  apply(response);
  return response;
}
```

`auth.authorizedFetch` fa lo stesso in una sola chiamata. Legge il token come `getAccessToken`, aggiunge l'header `Authorization` e, con DPoP, la prova:

```ts
const res = await auth.authorizedFetch(await cookies(), "https://api.acme.example/v1/orders");
```

Non passare l'access token a un Client Component. Chiama la tua API dal server e invia al browser solo i dati. Per verificare questi token nella tua API, vedi [Verifica dei token nella tua API](/docs/sdk/token-verification).

## Disconnetti

Crea un link a `/auth/logout`, oppure invia una `POST` a quella route da un form. L'SDK:

1. elimina il cookie di sessione;
2. revoca il refresh token;
3. 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, così un altro sito non può disconnettere i tuoi utenti. I browser che non inviano `Sec-Fetch-Site` possono disconnettere solo con una `POST` dalle tue pagine; preferisci un form che invia a `/auth/logout`.

## Token vincolati al mittente (DPoP)

Con DPoP (RFC 9449), il tuo access token è legato a una chiave che solo il tuo server possiede. Un token trapelato, per esempio da un log delle API, non può essere usato senza quella chiave.

1. Genera una sola chiave ES256 (P-256) per l'applicazione e conserva la JWK privata nel tuo secret store.
2. Passala come `dpop`:

```ts
export const auth = createKeenAuth({
  // ...the options above
  dpop: { privateKey: JSON.parse(process.env.KEENAUTH_DPOP_KEY!) },
});
```

L'SDK quindi:

- lega il codice di autorizzazione alla chiave (`dpop_jkt`, inviato con la richiesta di autorizzazione pushed);
- invia una prova DPoP con ogni richiesta di token e di refresh, e riprova una volta quando il server chiede un nuovo nonce;
- rifiuta una risposta di token il cui `token_type` non è `DPoP`.

Per chiamare la tua API, usa `auth.authorizedFetch`. Invia `Authorization: DPoP <token>` e una nuova prova con `ath` per ogni richiesta, e riprova una volta quando la tua API risponde `401` con `error="use_dpop_nonce"`. Se usi un tuo client HTTP, crea la prova con `auth.createDpopProof({ method, url, accessToken, nonce })`.

Usa la stessa chiave in ogni processo server: il tuo ambiente lega a essa i token e il refresh token. Se sostituisci la chiave, o attivi o disattivi DPoP, le sessioni esistenti terminano e gli utenti devono accedere di nuovo.

## Sessioni lato server

Di default l'intera sessione è sigillata nel cookie. Passa un `sessionStore` per mantenere la sessione sul server. Il cookie contiene allora solo un riferimento cifrato a un session id casuale.

```ts
import { createKeenAuth, memorySessionStore, type SessionStore } from "@keenauth/nextjs";

// Next.js bundles proxy.ts, route handlers and pages separately, and each bundle runs its own copy of this module.
// Keep the store on globalThis so that they all share one store in the server process.
const shared = globalThis as { keenauthSessions?: SessionStore };
const sessionStore = (shared.keenauthSessions ??= memorySessionStore());

export const auth = createKeenAuth({
  // ...the options above
  sessionStore,
});
```

Crea lo store in memoria una sola volta per processo, come sopra. Un `memorySessionStore()` chiamato a livello di modulo, senza `globalThis`, dà al proxy, a ogni route handler e alle pagine un proprio store vuoto: la sessione salvata dalla route di callback non viene trovata dal proxy o da una pagina, e l'utente risulta disconnesso. Uno store che mantiene i dati fuori dal processo (Redis, un database) non ha questo problema.

`memorySessionStore` mantiene le sessioni nella memoria di un processo e le perde al riavvio. Usalo per lo sviluppo e per server a istanza singola. Con più istanze o funzioni serverless, implementa l'interfaccia `SessionStore` su uno store condiviso come Redis o il tuo database:

```ts
interface SessionStore {
  get(id: string): Promise<SessionRecord | null>;
  set(id: string, record: SessionRecord): Promise<void>;
  delete(id: string): Promise<void>;
  deleteByLogout(target: { sub?: string; sid?: string }): Promise<void>;
}
// SessionRecord: { value: string; sub: string; sid?: string; expiresAt: number }
```

- `value` è comunque cifrato con il tuo `secret`, quindi una copia dello store non rivela alcun token. Non registrarlo nei log.
- `deleteByLogout` con `sid` elimina le sessioni con quel `sid`; con anche `sub`, entrambi devono corrispondere. Con solo `sub`, elimina ogni sessione di quell'utente.
- Elimina un record dopo `expiresAt`.
- Il repository del pacchetto ha un test di contratto (`clients/nextjs/test/store-contract.ts`) che il tuo store dovrebbe superare.

Se lo store fallisce, l'SDK lancia un `session_store_error` invece di considerare l'utente disconnesso. Disconnettersi elimina comunque il cookie.

## Logout back-channel

Con un `sessionStore`, l'SDK serve anche `/auth/backchannel-logout`. Quando una sessione termina in KeenAuth (per esempio, l'utente si disconnette da un'altra applicazione o un amministratore revoca la sessione), il tuo ambiente invia lì un logout token firmato. L'SDK elimina le sessioni corrispondenti, quindi la richiesta successiva dell'utente nella tua app non ha più una sessione.

1. Registra `https://app.acme.example/auth/backchannel-logout` come logout URI back-channel dell'applicazione.
2. Assicurati che la route sia raggiungibile da internet. Il matcher del proxy potrebbe includerla: il proxy la lascia passare.

L'SDK verifica il logout token come richiesto da OpenID Connect Back-Channel Logout 1.0: firma, issuer, audience, `iat` ed `exp`, l'evento di logout, `sub` o `sid`, assenza di `nonce` e il tipo `logout+jwt`. Risponde `200` in caso di successo e `400` altrimenti, e il tuo ambiente riprova una consegna non riuscita.

Il `jti` di ogni logout token viene ricordato fino alla scadenza del token, così un token catturato non può essere riprodotto. Un `jti` ripetuto riceve `200` senza agire di nuovo. La cache predefinita vive in un processo; con più istanze, passa una `logoutReplayCache` condivisa che implementi in modo atomico `remember(jti, expiresAt)` (per esempio Redis `SET NX`) e `forget(jti)`.

## Il cookie di sessione

- Il cookie è cifrato e autenticato con AES-256-GCM, usando una chiave derivata da `secret`. Un cookie modificato o copiato viene rifiutato.
- Si chiama `__Host-keenauth_session.0` e viene impostato con `Secure`, `HttpOnly`, `SameSite=Lax` e `Path=/`. Su `http://localhost` il prefisso `__Host-` e `Secure` vengono omessi.
- Una sessione dura al massimo 7 giorni (`sessionMaxAge`); il rinnovo non la prolunga.
- Le sessioni grandi vengono suddivise in fino a tre cookie.

Per cambiare il secret, passa una lista: `secret: [newSecret, oldSecret]`. Il primo secret cifra e tutti decifrano. Rimuovi quello vecchio dopo `sessionMaxAge`.

## Limiti attuali

- L'accesso con componenti integrati non è ancora disponibile in questo pacchetto.
- I rinnovi sono condivisi solo all'interno di un processo server. Con più istanze, due di esse possono rinnovare la stessa sessione contemporaneamente. L'impostazione dell'applicazione `refresh_reuse_interval_s` (predefinita 5 secondi, vedi [Aggiorna un'applicazione](/docs/api/management/applications/update)) permette al secondo rinnovo di riuscire; se la imposti a `0`, il secondo rinnovo termina la sessione.
- Senza un `sessionStore`, il logout back-channel non è disponibile; una sessione termina allora sul server quando il suo refresh token viene rifiutato.
