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
- Erstellen Sie im Dashboard eine Webanwendung in Ihrer Umgebung.
- Wählen Sie die Client-Authentifizierung
private_key_jwtund registrieren Sie Ihren öffentlichen Schlüssel. Ein Client-Secret funktioniert ebenfalls. - Fügen Sie die Redirect-URI
https://app.acme.example/auth/callbackund die Redirect-URI nach dem Abmeldenhttps://app.acme.example/hinzu. - 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
// 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
// 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
// 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 (
GEToderHEAD) wird mitreturnTonach/auth/loginweitergeleitet; - eine Anfrage an
/api/…oder mit einer anderen Methode erhält401mit 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:
// 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:
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:
// 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:
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.
Abmelden
Verlinken Sie auf /auth/logout, oder senden Sie ein POST an diese Route aus einem Formular. Das SDK:
- löscht das Sitzungs-Cookie;
- widerruft das Refresh-Token;
- 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.
- Erzeugen Sie einen ES256-(P-256-)Schlüssel für die Anwendung und bewahren Sie den privaten JWK in Ihrem Secret-Store auf.
- Übergeben Sie ihn als
dpop:
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_typenichtDPoPist.
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.
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:
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 }
valueist weiterhin mit Ihremsecretverschlüsselt, daher verrät eine Kopie des Stores kein Token. Protokollieren Sie ihn nicht.deleteByLogoutmitsidlöscht die Sitzungen mit diesersid; mit zusätzlichemsubmüssen beide übereinstimmen. Nur mitsublö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.
- Registrieren Sie
https://app.acme.example/auth/backchannel-logoutals Back-Channel-Logout-URI der Anwendung. - 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
secretabgeleiteten Schlüssel. Ein verändertes oder kopiertes Cookie wird abgelehnt. - Es heißt
__Host-keenauth_session.0und wird mitSecure,HttpOnly,SameSite=LaxundPath=/gesetzt. Aufhttp://localhostentfallen das Präfix__Host-undSecure. - 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) lässt die zweite Aktualisierung gelingen; setzen Sie sie auf0, beendet die zweite Aktualisierung die Sitzung. - Ohne
sessionStoreist Back-Channel-Logout nicht verfügbar; eine Sitzung endet dann auf dem Server, wenn ihr Refresh-Token verweigert wird.