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

  1. Erstellen Sie im Dashboard eine Webanwendung in Ihrer Umgebung.
  2. Wählen Sie die Client-Authentifizierung private_key_jwt und registrieren Sie Ihren öffentlichen Schlüssel. Ein Client-Secret funktioniert ebenfalls.
  3. Fügen Sie die Redirect-URI https://app.acme.example/auth/callback und die Redirect-URI nach dem Abmelden https://app.acme.example/ hinzu.
  4. 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:

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:

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:

  1. löscht das Sitzungs-Cookie;
  2. widerruft das Refresh-Token;
  3. 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.

  1. Erzeugen Sie einen ES256-(P-256-)Schlüssel für die Anwendung und bewahren Sie den privaten JWK in Ihrem Secret-Store auf.
  2. Ü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:

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 }

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.

  1. Registrieren Sie https://app.acme.example/auth/backchannel-logout als Back-Channel-Logout-URI der Anwendung.
  2. 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).

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