
# Next.js SDK

`@keenauth/nextjs` fügt einer Next.js-Anwendung mit App Router Anmeldung hinzu. Ihre Anwendung ist der OAuth-Client und hält alle Tokens auf dem Server. Der Browser erhält nur ein verschlüsseltes, HttpOnly-Sitzungs-Cookie und sieht niemals ein Access-, Refresh- oder ID-Token. Dies ist das Backend-for-Frontend-(BFF-)Muster. Das Paket hat keine Laufzeitabhängigkeiten. Es ist noch nicht auf npm veröffentlicht.

## Bevor Sie beginnen

1. Erstellen Sie im Dashboard eine Webanwendung in Ihrer Umgebung.
2. Wählen Sie die Client-Authentifizierung `private_key_jwt` und registrieren Sie Ihren öffentlichen Schlüssel. Ein Client-Secret funktioniert ebenfalls.
3. Fügen Sie die Redirect-URI `https://app.acme.example/auth/callback` und die Redirect-URI nach dem Abmelden `https://app.acme.example/` hinzu.
4. Erzeugen Sie ein Cookie-Secret mit mindestens 32 Zeichen, etwa mit `openssl rand -base64 32`. Bewahren Sie es in Ihrem Secret-Store auf, nicht im Repository.

## Instanz erstellen

```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` ist die öffentliche Adresse Ihrer Anwendung. Das SDK erstellt die Redirect-URI und alle Weiterleitungen daraus, niemals aus dem `Host`- oder den `X-Forwarded-*`-Headern der Anfrage. Unverschlüsseltes `http` wird nur für `localhost` akzeptiert.

Die Optionen werden beim Erstellen der Instanz geprüft, sodass ein Konfigurationsfehler den Start der Anwendung stoppt.

## Routen hinzufügen

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

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

Dies stellt drei Routen bereit:

| Route | Funktion |
|---|---|
| `/auth/login` | Startet die Anmeldung. Ein optionaler Query-Parameter `returnTo` legt fest, wohin der Benutzer danach gelangt. |
| `/auth/callback` | Schließt die Anmeldung ab und setzt das Sitzungs-Cookie. |
| `/auth/logout` | Meldet bei KeenAuth ab und kehrt zu Ihrer Anwendung zurück. |

Die Anmeldung verwendet den Authorization-Code-Flow mit PKCE. Die Anfrage wird an den Server übermittelt (PAR), wenn Ihre Umgebung dies unterstützt. Im Callback prüft das SDK `state`, `iss` und das ID-Token einschließlich seines `nonce`.

`returnTo` muss ein Pfad Ihrer eigenen Anwendung sein, etwa `/orders?tab=open`. Jeder andere Wert, etwa `//evil.example` oder eine vollständige URL, wird ignoriert, und der Benutzer gelangt zu `/`.

## Seiten schützen und Tokens aktualisieren

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

Für einen geschützten Pfad ohne Sitzung gilt:

- Eine Seitenanfrage (`GET` oder `HEAD`) wird mit `returnTo` nach `/auth/login` weitergeleitet;
- eine Anfrage an `/api/…` oder mit einer anderen Methode erhält `401` mit einem Problem-Dokument.

Läuft das Access-Token innerhalb von 60 Sekunden ab, aktualisiert der Proxy es. Jede Aktualisierung rotiert das Refresh-Token, und das neue Sitzungs-Cookie wird in der Antwort gesetzt. `requestHeaders` übergibt die neue Sitzung an die Seiten, die dieselbe Anfrage rendern.

Verweigert der Server das Refresh-Token, endet die Sitzung. Ein vorübergehender Serverfehler lässt die Sitzung unverändert.

Prüfen Sie die Sitzung außerdem in jeder Server Function und jedem Route Handler. Ein Matcher des Proxys kann einen Pfad übersehen.

## Sitzung lesen

In einer Server Component übergeben Sie den 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` enthält typisierte Claims des ID-Tokens: `subject`, `email`, `emailVerified`, `name`, `orgId`, `orgRoles` und weitere. Alle Claims, auch benutzerdefinierte, stehen in `session.user.claims`. Das Sitzungsobjekt enthält niemals ein Token.

Eine Server Component kann keine Cookies setzen. Das Lesen der Sitzung dort aktualisiert sie deshalb nie.

## Ihre API aufrufen

In einer Server Component holen Sie das Access-Token aus dem 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` schlägt fehl mit:

- `refresh_required`, wenn das Token kurz vor dem Ablauf steht und der Proxy es hätte aktualisieren sollen;
- `login_required`, wenn keine Sitzung vorhanden ist.

In einem Route Handler aktualisiert `updateSession` ebenfalls:

```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` erledigt dasselbe mit einem einzigen Aufruf. Es liest das Token wie `getAccessToken`, fügt den Header `Authorization` hinzu und bei DPoP den Nachweis:

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

Geben Sie das Access-Token niemals an eine Client Component weiter. Rufen Sie Ihre API vom Server aus auf und senden Sie dem Browser nur die Daten. Um diese Tokens in Ihrer API zu prüfen, siehe [Token in Ihrer API prüfen](/docs/sdk/token-verification).

## Abmelden

Verlinken Sie auf `/auth/logout`, oder senden Sie ein `POST` an diese Route aus einem Formular. Das SDK:

1. löscht das Sitzungs-Cookie;
2. widerruft das Refresh-Token;
3. schickt den Browser zum Logout-Endpunkt Ihrer Umgebung, der ihn zu Ihrer Anwendung zurückleitet.

Eine Anfrage, die der Browser als von einer anderen Website stammend kennzeichnet, wird abgelehnt, damit andere Websites Ihre Benutzer nicht abmelden können. Browser, die `Sec-Fetch-Site` nicht senden, können nur über ein `POST` von Ihren eigenen Seiten abmelden. Verwenden Sie bevorzugt ein Formular, das an `/auth/logout` sendet.

## Sender-gebundene Tokens (DPoP)

Mit DPoP (RFC 9449) ist Ihr Access-Token an einen Schlüssel gebunden, den nur Ihr Server besitzt. Ein entwendetes Token, etwa aus einem API-Log, lässt sich ohne diesen Schlüssel nicht verwenden.

1. Erzeugen Sie einen ES256-(P-256-)Schlüssel für die Anwendung und bewahren Sie den privaten JWK in Ihrem Secret-Store auf.
2. Übergeben Sie ihn als `dpop`:

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

Das SDK tut dann Folgendes:

- bindet den Authorization Code an den Schlüssel (`dpop_jkt`, mit der übermittelten Autorisierungsanfrage gesendet);
- sendet bei jeder Token- und Refresh-Anfrage einen DPoP-Nachweis und wiederholt die Anfrage einmal, wenn der Server einen neuen Nonce verlangt;
- verweigert eine Token-Antwort, deren `token_type` nicht `DPoP` ist.

Verwenden Sie zum Aufruf Ihrer API `auth.authorizedFetch`. Es sendet `Authorization: DPoP <token>` und für jede Anfrage einen neuen Nachweis mit `ath`. Antwortet Ihre API mit `401` und `error="use_dpop_nonce"`, wiederholt das SDK die Anfrage einmal. Wenn Sie Ihren eigenen HTTP-Client verwenden, erstellen Sie den Nachweis mit `auth.createDpopProof({ method, url, accessToken, nonce })`.

Verwenden Sie in jedem Serverprozess denselben Schlüssel: Ihre Umgebung bindet die Tokens und das Refresh-Token daran. Ersetzen Sie den Schlüssel oder schalten Sie DPoP ein oder aus, enden bestehende Sitzungen und Benutzer müssen sich erneut anmelden.

## Serverseitige Sitzungen

Standardmäßig wird die gesamte Sitzung im Cookie versiegelt. Übergeben Sie einen `sessionStore`, um die Sitzung stattdessen auf dem Server zu speichern. Das Cookie enthält dann nur eine verschlüsselte Referenz auf eine zufällige Sitzungs-ID.

```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,
});
```

Erstellen Sie den In-Memory-Store wie oben gezeigt einmal pro Prozess. Ein auf Modulebene ohne `globalThis` aufgerufenes `memorySessionStore()` gibt dem Proxy, jedem Route Handler und den Seiten jeweils einen eigenen leeren Store. Die Sitzung, die die Callback-Route speichert, finden dann weder der Proxy noch eine Seite, und der Benutzer erscheint als abgemeldet. Ein Store, der seine Daten außerhalb des Prozesses hält (Redis, eine Datenbank), hat dieses Problem nicht.

`memorySessionStore` speichert Sitzungen im Speicher eines Prozesses und verliert sie beim Neustart. Verwenden Sie ihn für die Entwicklung und für Server mit einer einzigen Instanz. Bei mehreren Instanzen oder Serverless-Funktionen implementieren Sie die Schnittstelle `SessionStore` für einen gemeinsamen Store wie Redis oder Ihre Datenbank:

```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` ist weiterhin mit Ihrem `secret` verschlüsselt, daher verrät eine Kopie des Stores kein Token. Protokollieren Sie ihn nicht.
- `deleteByLogout` mit `sid` löscht die Sitzungen mit dieser `sid`; mit zusätzlichem `sub` müssen beide übereinstimmen. Nur mit `sub` löscht es jede Sitzung dieses Benutzers.
- Entfernen Sie einen Datensatz nach `expiresAt`.
- Das Repository des Pakets enthält einen Vertragstest (`clients/nextjs/test/store-contract.ts`), den Ihr Store bestehen sollte.

Schlägt der Store fehl, wirft das SDK einen `session_store_error`, statt den Benutzer als abgemeldet zu behandeln. Das Abmelden löscht weiterhin das Cookie.

## Back-Channel-Logout

Mit einem `sessionStore` stellt das SDK außerdem `/auth/backchannel-logout` bereit. Endet eine Sitzung bei KeenAuth (etwa wenn sich der Benutzer bei einer anderen Anwendung abmeldet oder ein Administrator die Sitzung widerruft), sendet Ihre Umgebung dorthin ein signiertes Logout-Token. Das SDK löscht die passenden Sitzungen, sodass die nächste Anfrage des Benutzers in Ihrer Anwendung keine Sitzung hat.

1. Registrieren Sie `https://app.acme.example/auth/backchannel-logout` als Back-Channel-Logout-URI der Anwendung.
2. Stellen Sie sicher, dass die Route aus dem Internet erreichbar ist. Ihr Proxy-Matcher kann sie enthalten: Der Proxy lässt sie durch.

Das SDK prüft das Logout-Token so, wie es OpenID Connect Back-Channel Logout 1.0 verlangt: Signatur, Issuer, Audience, `iat` und `exp`, das Logout-Ereignis, `sub` oder `sid`, kein `nonce` und den Typ `logout+jwt`. Bei Erfolg antwortet es mit `200`, sonst mit `400`. Ihre Umgebung wiederholt eine fehlgeschlagene Zustellung.

Die `jti` jedes Logout-Tokens wird bis zu dessen Ablauf gespeichert, sodass ein abgefangenes Token nicht erneut verwendet werden kann. Eine wiederholte `jti` wird mit `200` beantwortet, ohne erneut zu wirken. Der Standard-Cache liegt in einem Prozess. Bei mehreren Instanzen übergeben Sie einen gemeinsamen `logoutReplayCache`, der `remember(jti, expiresAt)` atomar implementiert (zum Beispiel Redis `SET NX`) sowie `forget(jti)`.

## Das Sitzungs-Cookie

- Das Cookie wird mit AES-256-GCM verschlüsselt und authentifiziert, mit einem aus `secret` abgeleiteten Schlüssel. Ein verändertes oder kopiertes Cookie wird abgelehnt.
- Es heißt `__Host-keenauth_session.0` und wird mit `Secure`, `HttpOnly`, `SameSite=Lax` und `Path=/` gesetzt. Auf `http://localhost` entfallen das Präfix `__Host-` und `Secure`.
- Eine Sitzung dauert höchstens 7 Tage (`sessionMaxAge`); eine Aktualisierung verlängert sie nicht.
- Große Sitzungen werden auf bis zu drei Cookies aufgeteilt.

Um das Secret zu ändern, übergeben Sie eine Liste: `secret: [newSecret, oldSecret]`. Das erste Secret verschlüsselt, alle entschlüsseln. Entfernen Sie das alte nach `sessionMaxAge`.

## Aktuelle Einschränkungen

- Anmeldung mit eingebetteten Komponenten ist in diesem Paket noch nicht verfügbar.
- Aktualisierungen werden nur innerhalb eines Serverprozesses koordiniert. Bei mehreren Instanzen können zwei von ihnen dieselbe Sitzung gleichzeitig aktualisieren. Die Anwendungseinstellung `refresh_reuse_interval_s` (Standard 5 Sekunden, siehe [Eine Anwendung aktualisieren](/docs/api/management/applications/update)) lässt die zweite Aktualisierung gelingen; setzen Sie sie auf `0`, beendet die zweite Aktualisierung die Sitzung.
- Ohne `sessionStore` ist Back-Channel-Logout nicht verfügbar; eine Sitzung endet dann auf dem Server, wenn ihr Refresh-Token verweigert wird.
