
# SDK de Next.js

`@keenauth/nextjs` añade el inicio de sesión a una aplicación de Next.js con App Router. Tu aplicación es el cliente OAuth y mantiene todos los tokens en el servidor. El navegador solo recibe una cookie de sesión cifrada y HttpOnly, y nunca ve un token de acceso, de actualización ni de ID. Este es el patrón backend-for-frontend (BFF). El paquete no tiene dependencias en tiempo de ejecución. Todavía no se ha publicado en npm.

## Antes de empezar

1. En el panel, crea una aplicación web en tu entorno.
2. Elige la autenticación de cliente `private_key_jwt` y registra tu clave pública. También funciona un secreto de cliente.
3. Añade el URI de redirección `https://app.acme.example/auth/callback` y el URI de redirección tras cerrar sesión `https://app.acme.example/`.
4. Genera un secreto para la cookie de al menos 32 caracteres, por ejemplo con `openssl rand -base64 32`. Guárdalo en tu almacén de secretos, no en el repositorio.

## Crear la instancia

```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` es la dirección pública de tu aplicación. El SDK construye a partir de ella el URI de redirección y todas las redirecciones, nunca a partir de los encabezados `Host` o `X-Forwarded-*` de la solicitud. El `http` simple solo se acepta para `localhost`.

Las opciones se comprueban al crear la instancia, así que un error de configuración detiene la aplicación al arrancar.

## Añadir las rutas

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

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

Esto sirve tres rutas:

| Ruta | Qué hace |
|---|---|
| `/auth/login` | Inicia el inicio de sesión. Un parámetro de consulta opcional `returnTo` indica a dónde llega el usuario después. |
| `/auth/callback` | Completa el inicio de sesión y establece la cookie de sesión. |
| `/auth/logout` | Cierra la sesión en KeenAuth y vuelve a tu aplicación. |

El inicio de sesión usa el flujo de código de autorización con PKCE. La solicitud se envía al servidor (PAR) cuando tu entorno lo ofrece. En el callback, el SDK comprueba `state`, `iss` y el token de ID, incluido su `nonce`.

`returnTo` debe ser una ruta de tu propia aplicación, como `/orders?tab=open`. Cualquier otro valor, como `//evil.example` o una URL completa, se ignora y el usuario llega a `/`.

## Proteger páginas y renovar tokens

```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).*)"],
};
```

Para una ruta protegida sin sesión:

- una solicitud de página (`GET` o `HEAD`) se redirige a `/auth/login` con `returnTo`;
- una solicitud a `/api/…`, o con otro método, recibe `401` con un documento de problema.

Cuando el token de acceso caduca en menos de 60 segundos, el proxy lo renueva. Cada renovación rota el token de actualización y la nueva cookie de sesión se establece en la respuesta. `requestHeaders` pasa la nueva sesión a las páginas que se renderizan en la misma solicitud.

Si el servidor rechaza el token de actualización, la sesión termina. Un error temporal del servidor deja la sesión sin cambios.

Comprueba también la sesión dentro de cada Server Function y route handler. El matcher del proxy puede no cubrir una ruta.

## Leer la sesión

En un Server Component, pasa el almacén de cookies:

```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 las claims tipadas del token de ID: `subject`, `email`, `emailVerified`, `name`, `orgId`, `orgRoles` y otras. Todas las claims, incluidas las personalizadas, están en `session.user.claims`. El objeto de sesión nunca contiene un token.

Un Server Component no puede establecer cookies, así que leer la sesión allí nunca la renueva.

## Llamar a tu API

En un Server Component, toma el token de acceso del almacén de cookies:

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

`getAccessToken` falla con:

- `refresh_required` cuando el token está cerca de caducar y el proxy debería haberlo renovado;
- `login_required` cuando no hay sesión.

En un route handler, `updateSession` también renueva:

```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` hace lo mismo en una sola llamada. Lee el token como `getAccessToken`, añade el encabezado `Authorization` y, con DPoP, la prueba:

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

No pases el token de acceso a un Client Component. Llama a tu API desde el servidor y envía al navegador solo los datos. Para verificar estos tokens en tu API, consulta [Verificar tokens en tu API](/docs/sdk/token-verification).

## Cerrar sesión

Enlaza a `/auth/logout`, o envía un `POST` a esa ruta desde un formulario. El SDK:

1. elimina la cookie de sesión;
2. revoca el token de actualización;
3. envía el navegador al endpoint de cierre de sesión de tu entorno, que lo devuelve a tu aplicación.

Se rechaza una solicitud que el navegador marca como procedente de otro sitio, así que otro sitio no puede cerrar la sesión de tus usuarios. Los navegadores que no envían `Sec-Fetch-Site` solo pueden cerrar sesión mediante un `POST` desde tus propias páginas; prefiere un formulario que envíe a `/auth/logout`.

## Tokens con restricción de remitente (DPoP)

Con DPoP (RFC 9449), tu token de acceso queda vinculado a una clave que solo tiene tu servidor. Un token filtrado, por ejemplo desde un registro de la API, no puede usarse sin esa clave.

1. Genera una clave ES256 (P-256) para la aplicación y guarda la JWK privada en tu almacén de secretos.
2. Pásala como `dpop`:

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

El SDK entonces:

- vincula el código de autorización a la clave (`dpop_jkt`, enviado con la solicitud de autorización empujada);
- envía una prueba DPoP con cada solicitud de token y de renovación, y reintenta una vez cuando el servidor pide un nuevo nonce;
- rechaza una respuesta de token cuyo `token_type` no sea `DPoP`.

Para llamar a tu API, usa `auth.authorizedFetch`. Envía `Authorization: DPoP <token>` y una nueva prueba con `ath` en cada solicitud, y reintenta una vez cuando tu API responde `401` con `error="use_dpop_nonce"`. Si usas tu propio cliente HTTP, crea la prueba con `auth.createDpopProof({ method, url, accessToken, nonce })`.

Usa la misma clave en todos los procesos del servidor: tu entorno vincula a ella los tokens y el token de actualización. Si reemplazas la clave, o activas o desactivas DPoP, las sesiones existentes terminan y los usuarios deben iniciar sesión de nuevo.

## Sesiones en el servidor

De forma predeterminada, toda la sesión se sella dentro de la cookie. Pasa un `sessionStore` para guardar la sesión en el servidor. La cookie contendrá entonces solo una referencia cifrada a un identificador de sesión aleatorio.

```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 el almacén en memoria una vez por proceso, como arriba. Un `memorySessionStore()` llamado a nivel de módulo, sin `globalThis`, da al proxy, a cada route handler y a las páginas su propio almacén vacío: la sesión que guarda la ruta de callback no la encuentran el proxy ni una página, y el usuario aparece como no autenticado. Un almacén que guarda sus datos fuera del proceso (Redis, una base de datos) no tiene este problema.

`memorySessionStore` guarda las sesiones en la memoria de un proceso y las pierde al reiniciar. Úsalo para desarrollo y servidores de una sola instancia. Con varias instancias o funciones serverless, implementa la interfaz `SessionStore` sobre un almacén compartido como Redis o tu base de datos:

```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` sigue cifrado con tu `secret`, así que una copia del almacén no revela ningún token. No lo registres en logs.
- `deleteByLogout` con `sid` elimina las sesiones con ese `sid`; si además se indica `sub`, ambos deben coincidir. Con solo `sub`, elimina todas las sesiones de ese usuario.
- Elimina un registro después de `expiresAt`.
- El repositorio del paquete incluye una prueba de contrato (`clients/nextjs/test/store-contract.ts`) que tu almacén debería superar.

Si el almacén falla, el SDK lanza un `session_store_error` en lugar de tratar al usuario como no autenticado. Cerrar sesión sigue eliminando la cookie.

## Cierre de sesión de canal trasero

Con un `sessionStore`, el SDK también sirve `/auth/backchannel-logout`. Cuando una sesión termina en KeenAuth (por ejemplo, el usuario cierra sesión en otra aplicación o un administrador revoca la sesión), tu entorno envía allí un token de cierre de sesión firmado. El SDK elimina las sesiones correspondientes, de modo que la siguiente solicitud del usuario en tu aplicación no tiene sesión.

1. Registra `https://app.acme.example/auth/backchannel-logout` como URI de cierre de sesión de canal trasero de la aplicación.
2. Asegúrate de que la ruta sea accesible desde internet. El matcher de tu proxy puede incluirla: el proxy la deja pasar.

El SDK comprueba el token de cierre de sesión tal como exige OpenID Connect Back-Channel Logout 1.0: firma, emisor, audiencia, `iat` y `exp`, el evento de cierre de sesión, `sub` o `sid`, ausencia de `nonce` y el tipo `logout+jwt`. Responde `200` si todo es correcto y `400` en caso contrario, y tu entorno reintenta una entrega fallida.

El `jti` de cada token de cierre de sesión se recuerda hasta que el token caduca, así que un token capturado no puede reutilizarse. Un `jti` repetido recibe `200` sin volver a actuar. La caché predeterminada vive en un proceso; con varias instancias, pasa un `logoutReplayCache` compartido que implemente `remember(jti, expiresAt)` de forma atómica (por ejemplo, `SET NX` en Redis) y `forget(jti)`.

## La cookie de sesión

- La cookie se cifra y autentica con AES-256-GCM, usando una clave derivada de `secret`. Una cookie modificada o copiada se rechaza.
- Se llama `__Host-keenauth_session.0` y se establece con `Secure`, `HttpOnly`, `SameSite=Lax` y `Path=/`. En `http://localhost` se omiten el prefijo `__Host-` y `Secure`.
- Una sesión dura como máximo 7 días (`sessionMaxAge`); renovar no la extiende.
- Las sesiones grandes se dividen en hasta tres cookies.

Para cambiar el secreto, pasa una lista: `secret: [newSecret, oldSecret]`. El primer secreto cifra y todos descifran. Elimina el antiguo después de `sessionMaxAge`.

## Límites actuales

- El inicio de sesión con componentes embebidos todavía no está disponible en este paquete.
- Las renovaciones se comparten solo dentro de un proceso del servidor. Con varias instancias, dos de ellas pueden renovar la misma sesión a la vez. El ajuste de la aplicación `refresh_reuse_interval_s` (5 segundos por defecto, consulta [Actualizar una aplicación](/docs/api/management/applications/update)) permite que la segunda renovación tenga éxito; si lo defines en `0`, la segunda renovación termina la sesión.
- Sin un `sessionStore`, el cierre de sesión de canal trasero no está disponible; en ese caso, una sesión termina en el servidor cuando se rechaza su token de actualización.
