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. Um die Management-API aufzurufen, siehe das Node.js-Management-SDK.
Bevor Sie beginnen
- Erstellen Sie im Dashboard in Ihrer Umgebung eine Webanwendung.
- Wählen Sie die Client-Authentifizierung: ein Client-Secret (
client_secret_basic) oderprivate_key_jwtmit einem Schlüsselpaar, dessen öffentlichen Schlüssel Sie bei der Anwendung registrieren.private_key_jwtwird empfohlen, weil kein gemeinsames Geheimnis Ihren Server verlässt. - Fügen Sie die Redirect-URI
https://app.acme.example/auth/callbackund die Post-Logout-Redirect-URIhttps://app.acme.example/hinzu. - 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 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.
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 (
GEToderHEAD) ohne Sitzung wird mitreturnToauf/auth/loginumgeleitet; - eine Anfrage an
/api/…oder mit einer anderen Methode erhält401mit einem Problem-Dokument. Übergeben Sie{ api: true }, um auf jedem Pfad mit401zu 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:nulloder der angemeldete Benutzer.session.userenthält typisierte ID-Token-Claims wiesubject,email,name,orgIdundorgRoles; alle Claims stehen insession.user.claims. Das Sitzungsobjekt enthält niemals ein Token.accessToken: ein gültiges Access-Token für serverseitige Aufrufe odernull.
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:
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:
- löscht das Sitzungscookie (und die gespeicherte Sitzung, falls Sie einen Store verwenden);
- widerruft das Refresh-Token;
- 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:
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 auf0, beendet die zweite Aktualisierung die Sitzung.