
# Fastify-Sitzungen

`@keenauth/node/bff` fügt einer Fastify-5-Anwendung die Anmeldung hinzu. Ihr Server ist der OAuth-Client und bewahrt alle Tokens auf dem Server auf. Der Browser erhält nur ein verschlüsseltes, HttpOnly-Sitzungscookie und sieht niemals ein Access-, Refresh- oder ID-Token. Dies ist das Backend-for-Frontend-(BFF)-Muster. Das Paket importiert Fastify nicht und fügt daher keine Abhängigkeit hinzu. Es ist noch nicht auf npm veröffentlicht.

Um stattdessen eine API zu schützen, die Bearer-Tokens entgegennimmt, siehe [Ihre API schützen](/docs/sdk/protect-api).

## Bevor Sie beginnen

1. Erstellen Sie im Dashboard eine Web-Anwendung in Ihrer Umgebung.
2. Wählen Sie die Client-Authentifizierung: ein Client-Secret (`client_secret_basic`) oder `private_key_jwt` mit einem Schlüsselpaar, dessen öffentlichen Schlüssel Sie bei der Anwendung registrieren. `private_key_jwt` wird empfohlen, weil kein gemeinsames Geheimnis Ihren Server verlässt.
3. Fügen Sie die Redirect-URI `https://app.acme.example/auth/callback` und die Post-Logout-Redirect-URI `https://app.acme.example/` hinzu.
4. Erzeugen Sie ein Cookie-Secret mit mindestens 32 Zeichen, zum Beispiel mit `openssl rand -base64 32`. Bewahren Sie es in Ihrem Secret-Store auf, nicht im Repository.

## Einrichtung

```ts
import Fastify from "fastify";
import { createBff, fastifyRequireAuth, fastifySession } from "@keenauth/node/bff";

const bff = createBff({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "client_secret_basic", clientSecret: process.env.KEENAUTH_CLIENT_SECRET! },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});

const app = Fastify();
fastifySession(app, bff); // on the root instance, so it applies to every route

app.get("/orders", { preHandler: fastifyRequireAuth(bff) }, async (request) => {
  return `Signed in as ${request.keenauth!.session!.user.email}`;
});
```

Bei `private_key_jwt` übergeben Sie den privaten Schlüssel statt des Secrets: `clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") }`. Das SDK signiert für jede Anfrage an den Token- und PAR-Endpunkt eine neue Client-Assertion. Liegt der Schlüssel in einem KMS oder HSM, übergeben Sie `clientAuth: { method: "private_key_jwt", assertion }`, wobei `assertion()` jedes Mal eine neue signierte Client-Assertion zurückgibt. Die [Express-Seite](/docs/sdk/express) beschreibt die Assertion und die unterstützten Algorithmen.

Rufen Sie `fastifySession` auf der Root-Instanz auf. Innerhalb eines gekapselten Plugins gilt es nur für die Routen dieses Plugins.

`appBaseUrl` ist die öffentliche Adresse Ihrer App. Das SDK erstellt daraus die Redirect-URI und alle Weiterleitungen, niemals aus dem `Host`- oder `X-Forwarded-*`-Header der Anfrage; daher wirkt sich die Option `trustProxy` nicht darauf aus. Einfaches `http` wird nur für `localhost` akzeptiert.

Die Optionen werden beim Aufruf von `createBff` geprüft, sodass ein Konfigurationsfehler die App beim Start anhält.

## Routen

`fastifySession` fügt drei Routen hinzu und liest die Sitzung in einem `onRequest`-Hook bei jeder anderen Anfrage:

| Route | Was sie bewirkt |
|---|---|
| `GET /auth/login` | Startet die Anmeldung. Optionale Query-Parameter: `returnTo` (wohin der Benutzer gelangt) und `organization` (Anmeldung bei einer Organisation). |
| `GET /auth/callback` | Schließt die Anmeldung ab und setzt das Sitzungscookie. |
| `GET` oder `POST /auth/logout` | Meldet bei KeenAuth ab und leitet zurück zu Ihrer App. |

Die Anmeldung verwendet den Authorization-Code-Flow mit PKCE (S256). Die Anfrage wird an den Server übermittelt (PAR), wenn Ihre Umgebung das unterstützt. Beim Callback prüft das SDK `state`, `iss` und das ID-Token einschließlich seines `nonce`. Jede erfolgreiche Anmeldung erzeugt eine neue Sitzung; die alte wird verworfen.

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

## Routen schützen

`fastifyRequireAuth(bff)` ist ein `preHandler`, der eine Anfrage nur mit einer Sitzung durchlässt:

- eine Seitenanfrage (`GET` oder `HEAD`) ohne Sitzung wird mit `returnTo` auf `/auth/login` umgeleitet;
- eine Anfrage an `/api/…` oder mit einer anderen Methode erhält `401` mit einem Problem-Dokument. Übergeben Sie `{ api: true }`, um auf jedem Pfad mit `401` zu antworten.

Bei `POST`, `PUT`, `PATCH`, `DELETE` und anderen unsicheren Methoden muss der Browser angeben, dass die Anfrage von Ihrer App stammt: `Sec-Fetch-Site: same-origin` oder ein `Origin`-Header, der dem Origin Ihrer App entspricht. Andere Anfragen erhalten `403`. Dies schützt zusätzlich zu `SameSite=Lax` vor Cross-Site-Request-Forgery.

## Sitzung verwenden und Ihre API aufrufen

`request.keenauth` enthält:

- `session`: `null` oder der angemeldete Benutzer. `session.user` enthält typisierte ID-Token-Claims wie `subject`, `email`, `name`, `orgId` und `orgRoles`; alle Claims stehen in `session.user.claims`. Das Sitzungsobjekt enthält niemals ein Token.
- `accessToken`: ein gültiges Access-Token für serverseitige Aufrufe oder `null`.

```ts
app.get("/api/orders", { preHandler: fastifyRequireAuth(bff) }, async (request, reply) => {
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${request.keenauth!.accessToken}` },
  });
  return reply.code(upstream.status).send(await upstream.json());
});
```

In TypeScript deklarieren Sie die Eigenschaft einmalig:

```ts
import type { SessionContext } from "@keenauth/node/bff";

declare module "fastify" {
  interface FastifyRequest {
    keenauth?: SessionContext | null;
  }
}
```

Läuft das Access-Token innerhalb von 60 Sekunden ab, erneuert das SDK es, bevor Ihr Handler ausgeführt wird. Jede Erneuerung rotiert das Refresh-Token und setzt das neue Sitzungscookie in der Antwort. Lehnt der Server das Refresh-Token ab, endet die Sitzung. Ein vorübergehender Serverfehler lässt die Sitzung unverändert.

Senden Sie das Access-Token nicht an den Browser.

## Organisation wechseln

`bff.switchOrganizationPath("org_01j9…", "/dashboard")` gibt einen Link zu `/auth/login` zurück, der den Benutzer in dieser Organisation erneut anmeldet. KeenAuth prüft die Mitgliedschaft. Nach der Anmeldung enthält `session.user.orgId` die Organisation.

## Abmelden

Senden Sie aus einem Formular ein `POST` an `/auth/logout`. Das SDK:

1. löscht das Sitzungscookie (und die gespeicherte Sitzung, falls Sie einen Store verwenden);
2. widerruft das Refresh-Token;
3. schickt den Browser zum Logout-Endpunkt Ihrer Umgebung, der ihn zu Ihrer App zurückleitet.

Eine Anfrage, die der Browser als von einer anderen Seite stammend kennzeichnet, wird abgelehnt. Browser, die kein `Sec-Fetch-Site` senden, können sich nur mit einem `POST` von Ihren eigenen Seiten abmelden.

## Sitzungsspeicher

Standardmäßig steht die gesamte Sitzung im Cookie, verschlüsselt und mit AES-256-GCM authentifiziert, mit einem aus `secret` abgeleiteten Schlüssel. Ein geändertes oder kopiertes Cookie wird abgelehnt. Das Cookie heißt `__Host-keenauth_session.0` und wird mit `Secure`, `HttpOnly`, `SameSite=Lax` und `Path=/` gesetzt. Auf `http://localhost` entfallen das `__Host-`-Präfix und `Secure`. Große Sitzungen werden auf bis zu drei Cookies aufgeteilt.

Um Tokens auf dem Server zu halten, übergeben Sie einen Store:

```ts
import { createBff, type SessionStore } from "@keenauth/node/bff";

const store: SessionStore = {
  get: async (id) => JSON.parse((await redis.get(`sess:${id}`)) ?? "null"),
  set: async (id, session, ttl) => void (await redis.set(`sess:${id}`, JSON.stringify(session), { EX: ttl })),
  delete: async (id) => void (await redis.del(`sess:${id}`)),
};
const bff = createBff({ /* … */ store });
```

Das Cookie enthält dann nur eine verschlüsselte zufällige Sitzungs-ID. Jede Anmeldung erzeugt eine neue ID und löscht den bisherigen Datensatz. Der Store enthält Tokens, daher beschränken Sie den Zugriff darauf. `MemorySessionStore` ist nur für Entwicklung und Tests gedacht.

Eine Sitzung dauert höchstens 7 Tage (`sessionMaxAge`); Erneuern verlängert sie nicht. Um das Secret zu ändern, übergeben Sie eine Liste: `secret: [newSecret, oldSecret]`. Entfernen Sie das alte nach `sessionMaxAge`.

## Aktuelle Einschränkungen

- DPoP-gebundene Tokens und Back-Channel-Logout werden noch nicht unterstützt.
- Erneuerungen werden nur innerhalb eines Server-Prozesses geteilt. Bei mehreren Instanzen lässt die Anwendungs-Einstellung `refresh_reuse_interval_s` (Standard 5 Sekunden) eine parallele Erneuerung erfolgreich sein; setzen Sie sie auf `0`, beendet die zweite Erneuerung die Sitzung.
