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.

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

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

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:

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:

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:

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