
# Express-Sitzungen

`@keenauth/node/bff` fügt einer Express-4- oder Express-5-Anwendung das Anmelden hinzu. Ihr Server ist der OAuth-Client und bewahrt alle Token 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 Express nicht und fügt deshalb keine Abhängigkeit hinzu. Es ist noch nicht auf npm veröffentlicht.

Um stattdessen eine API zu schützen, die Bearer-Token entgegennimmt, siehe [Ihre API schützen](/docs/sdk/protect-api). Um die Management-API aufzurufen, siehe das [Node.js-Management-SDK](/docs/sdk/node).

## Bevor Sie beginnen

1. Erstellen Sie im Dashboard in Ihrer Umgebung eine Webanwendung.
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 express from "express";
import { createBff, expressRequireAuth, expressSession } 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 = express();
app.use(expressSession(bff));

app.get("/orders", expressRequireAuth(bff), (req, res) => {
  const { session } = req.keenauth!;
  res.send(`Signed in as ${session!.user.email}`);
});
```

Bei `private_key_jwt` übergeben Sie den privaten Schlüssel anstelle des Secrets. Das SDK signiert für jede Anfrage an den Token- und PAR-Endpunkt eine neue Client-Assertion (`aud` ist der Issuer, `jti` ist zufällig, gültig für 120 Sekunden, `kid` ist der RFC-7638-Thumbprint des Schlüssels). Der Algorithmus richtet sich nach dem Schlüssel: `RS256` für RSA (oder `alg: "PS256"`), `ES256` für P-256 und `EdDSA` für Ed25519.

```ts
import { readFileSync } from "node:fs";

const bff = createBff({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
```

Wenn der Schlüssel in einem KMS oder HSM liegt, übergeben Sie stattdessen `clientAuth: { method: "private_key_jwt", assertion }`, wobei `assertion()` jedes Mal eine neue signierte Client-Assertion zurückgibt. Übergeben Sie entweder `privateKey` oder `assertion`, nicht beides.

`appBaseUrl` ist die öffentliche Adresse Ihrer App. Das SDK erstellt die Redirect-URI und alle Weiterleitungen daraus, niemals aus dem `Host`- oder den `X-Forwarded-*`-Headern der Anfrage, daher hat die Einstellung `trust proxy` darauf keinen Einfluss. Unverschlüsseltes `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

`expressSession` stellt drei Routen bereit und liest bei jeder anderen Anfrage die Sitzung aus:

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

Die Anmeldung verwendet den Authorization-Code-Flow mit PKCE (S256). Die Anfrage wird an den Server übermittelt (PAR), sofern Ihre Umgebung dies anbietet. 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, zum Beispiel `//evil.example` oder eine vollständige URL, wird ignoriert, und der Benutzer gelangt zu `/`.

## Routen schützen

`expressRequireAuth(bff)` lässt eine Anfrage nur mit einer Sitzung durch:

- 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 Ursprung 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

`req.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", expressRequireAuth(bff), async (req, res) => {
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${req.keenauth!.accessToken}` },
  });
  res.status(upstream.status).json(await upstream.json());
});
```

In TypeScript deklarieren Sie die Eigenschaft einmalig:

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

declare module "express-serve-static-core" {
  interface Request {
    keenauth?: SessionContext | null;
  }
}
```

Läuft das Access-Token innerhalb von 60 Sekunden ab, aktualisiert das SDK es, bevor Ihr Handler ausgeführt wird. Jede Aktualisierung 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 erneut in dieser Organisation 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. leitet den Browser zum Logout-Endpunkt Ihrer Umgebung weiter, der ihn zu Ihrer App zurückbringt.

Eine Anfrage, die der Browser als von einer anderen Website 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 liegt die gesamte Sitzung im Cookie, verschlüsselt und mit AES-256-GCM authentifiziert, mit einem aus `secret` abgeleiteten Schlüssel. Ein verä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` werden das Präfix `__Host-` und `Secure` weggelassen. Große Sitzungen werden auf bis zu drei Cookies aufgeteilt.

Um Token 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 noch eine verschlüsselte zufällige Sitzungs-ID. Jede Anmeldung erzeugt eine neue ID und löscht den vorherigen Datensatz. Der Store enthält Token, daher schränken Sie den Zugriff darauf ein. `MemorySessionStore` ist nur für Entwicklung und Tests gedacht.

Eine Sitzung dauert höchstens 7 Tage (`sessionMaxAge`); eine Aktualisierung 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 Token und Back-Channel-Logout werden noch nicht unterstützt.
- Aktualisierungen werden nur innerhalb eines Serverprozesses geteilt. Bei mehreren Instanzen lässt die Anwendungseinstellung `refresh_reuse_interval_s` (Standard 5 Sekunden) eine parallele Aktualisierung gelingen; setzen Sie sie auf `0`, beendet die zweite Aktualisierung die Sitzung.
