Anmeldung zu einer React-Router-App hinzufügen
Diese Schnellstart-Anleitung fügt einer React-Router-8-App im Framework-Modus die KeenAuth-Anmeldung hinzu, mit dem Standard-OpenID-Connect-Client openid-client. Die Loader und Actions laufen auf dem Server, daher ist Ihre App der OAuth-Client: Sie hält alle Token in einer serverseitigen Sitzung, und der Browser erhält nur das HTML und ein signiertes Sitzungs-ID-Cookie. Sie fügen die Anmelde-Routen hinzu, schützen eine Seite, rufen eine geschützte API mit dem Zugriffstoken des Benutzers auf, melden sich ab und testen die App offline.
Für eine Single-Page-App ohne Server siehe Anmeldung zu einer React-Single-Page-App hinzufügen.
Die App hat diese Routen:
| Route | Was sie macht |
|---|---|
GET / |
Öffentliche Startseite: eine Schaltfläche zum Anmelden oder der Name des Benutzers und eine Schaltfläche zum Abmelden |
GET /dashboard |
Benötigt eine Sitzung. Zeigt den Benutzer und die Nachrichten, die Ihre API zurückgibt |
POST /login |
Startet die Anmeldung. Nur von einer Seite dieser App |
GET /auth/callback |
Prüft die Antwort, tauscht den Code aus und startet die Sitzung |
POST /logout |
Beendet die Sitzung hier und bei KeenAuth. Nur von einer Seite dieser App |
Bevor Sie beginnen
- Eine KeenAuth-Umgebung. Ihr Issuer sieht aus wie
https://your-environment.eu.keenauth.app. - Eine Webanwendung in der Umgebung (Anwendung erstellen) mit einem Client-Secret (
client_secret_basic). Registrieren Sie in einerdevelopment-Umgebung die Redirect-URIhttp://localhost/auth/callbackund die Post-Logout-Redirect-URIhttp://localhost/. Eine Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa5173. - Eine API, die in der Umgebung registriert ist (API erstellen), mit dem Scope
messages:read. Ihridentifier, etwahttps://api.example.com, ist die Audience ihrer Zugriffstoken. Die Express-Schnellstart-Anleitung erstellt eine API, die diese Token akzeptiert. - Node.js 22.22 oder neuer und eine React-Router-App im Framework-Modus (
npx create-react-router@8.5.0).
1. Installieren
npm install --save-exact openid-client@6.8.8
openid-client implementiert den OpenID-Connect-Relying-Party: Discovery, den Authorization-Code-Flow mit PKCE, die iss-Prüfung nach RFC 9207 und die Prüfungen des ID-Tokens. Das Beispiel fixiert jedes Paket exakt und installiert aus package-lock.json mit npm ci --ignore-scripts, sodass während der Installation kein Paketskript läuft.
2. Konfigurieren
Die App liest ihre Konfiguration ausschließlich aus Umgebungsvariablen. Legen Sie sie in .env ab, die npm run dev und npm start mit node --env-file=.env laden. Committen Sie diese Datei niemals.
| Variable | Erforderlich | Bedeutung |
|---|---|---|
KEENAUTH_ISSUER |
ja | Der Issuer Ihrer Umgebung, https |
KEENAUTH_CLIENT_ID |
ja | Die client_id der Webanwendung |
KEENAUTH_CLIENT_SECRET |
ja | Deren Client-Secret |
APP_BASE_URL |
ja | Die öffentliche URL der App. Die Redirect-URI wird daraus gebildet. http nur für localhost, 127.0.0.1 oder [::1] |
SESSION_SECRET |
ja | Mindestens 32 Zeichen; signiert das Sitzungs-Cookie. Erzeugen Sie einen mit openssl rand -base64 32 |
API_URL |
ja | Die API, die die App aufruft, etwa http://localhost:8080 |
KEENAUTH_API_AUDIENCE |
ja | Der Identifier dieser API in KeenAuth |
loadConfig in app/lib/config.server.ts prüft diese Werte bei der ersten Anfrage. Fehlt ein Wert oder ist er unbrauchbar, bricht die Anfrage ab, und keine Fehlermeldung enthält einen Wert:
const issuer = url("KEENAUTH_ISSUER", false);
const appBaseUrl = url("APP_BASE_URL", true).origin;
const sessionSecret = need("SESSION_SECRET");
if (sessionSecret.length < 32) throw new Error("SESSION_SECRET must be at least 32 characters");
Dateien, die auf .server.ts enden, gelangen nie in das Browser-Bundle. React Router bricht den Build ab, wenn eine Seite eine solche Datei für den Browser importiert.
3. OpenID-Connect-Client erstellen
app/lib/keenauth.server.ts erstellt den Client einmalig aus dem Discovery-Dokument (<issuer>/.well-known/openid-configuration):
export function keenauth(): Promise<client.Configuration> {
discovered ??= client
.discovery(config().issuer, config().clientId, undefined, client.ClientSecretBasic(config().clientSecret), {
// Also check the ID token signature with your environment's keys (jwks_uri), not only the TLS connection.
execute: [client.enableNonRepudiationChecks],
})
Standardmäßig vertraut openid-client einem ID-Token, das direkt über TLS vom Token-Endpunkt kommt. enableNonRepudiationChecks prüft zusätzlich dessen Signatur mit den Schlüsseln, die Ihre Umgebung veröffentlicht.
4. Sitzung auf dem Server halten
Die Sitzung liegt in Dateien auf dem Server. Das Cookie enthält nur eine signierte Sitzungs-ID:
storage ??= createFileSessionStorage<SessionData>({
dir: ".sessions",
cookie: {
name: "__keenauth_session",
httpOnly: true,
sameSite: "lax",
// Secure whenever the app is served over https; plain http only for localhost.
secure: config().redirectUri.startsWith("https:"),
path: "/",
maxAge: 8 * 60 * 60,
secrets: [config().sessionSecret],
},
});
Ein Dateispeicher bedient einen einzelnen Server. Bei mehreren Servern schreiben Sie einen Sitzungsspeicher mit createSessionStorage und einem gemeinsamen Speicher, etwa Redis. Verwenden Sie keine Cookie-Sitzungsspeicherung für Token: Zwei Token können das Cookie-Limit von 4 KB überschreiten, und der Browser würde sie speichern.
5. Anmeldung starten
Die Routen stehen in app/routes.ts:
export default [
index("routes/home.tsx"),
route("dashboard", "routes/dashboard.tsx"),
route("login", "routes/login.ts"),
route("auth/callback", "routes/callback.ts"),
route("logout", "routes/logout.ts"),
] satisfies RouteConfig;
Die Anmeldung ist ein Formular, das an /login sendet:
<Form method="post" action="/login" reloadDocument>
<button type="submit">Sign in</button>
</Form>
Die Action erzeugt für jede Anmeldung einen neuen state, nonce und PKCE-Verifier und hält sie in der Sitzung:
export async function action({ request }: Route.ActionArgs) {
assertSameOrigin(request);
const session = await getSession(request);
const transaction = newTransaction();
session.set("transaction", transaction);
const url = await authorizationUrl(transaction);
return redirect(url.href, { headers: { "Set-Cookie": await sessions().commitSession(session) } });
}
Die Autorisierungsanfrage sendet die S256-Challenge des Verifiers und den Identifier Ihrer API als resource (RFC 8707). Ohne resource ist das Zugriffstoken nicht für Ihre API bestimmt, und die API weist es ab.
return client.buildAuthorizationUrl(await keenauth(), {
redirect_uri: config().redirectUri,
// messages:read is a scope of your API; the example API requires it on /api/messages.
scope: "openid profile email messages:read",
state: transaction.state,
nonce: transaction.nonce,
// PKCE (RFC 7636): S256 of the verifier that only this session knows.
code_challenge: await client.calculatePKCECodeChallenge(transaction.codeVerifier),
code_challenge_method: "S256",
// RFC 8707: an access token for your API.
resource: config().apiAudience,
});
POST nur von Ihren eigenen Seiten akzeptieren
React Router weist einen seitenübergreifenden POST an die Action einer Seite ab. /login und /logout sind jedoch Ressourcen-Routen (eine Action ohne Seite), und für diese liegt die Prüfung bei Ihnen. assertSameOrigin akzeptiert den POST nur, wenn sein Origin-Header (oder ohne ihn Referer) APP_BASE_URL nennt:
const app = new URL(config().redirectUri).origin;
const source = request.headers.get("Origin") ?? request.headers.get("Referer");
6. Callback verarbeiten
Der Callback nimmt zuerst die Transaktion aus der Sitzung, damit ein state nur einmal funktioniert:
const old = await getSession(request);
const transaction = old.get("transaction");
// The transaction works once: it leaves the session before anything else happens.
old.unset("transaction");
authorizationCodeGrant prüft state und das iss, das Ihre Umgebung in jeder Autorisierungsantwort sendet (RFC 9207). Dann tauscht sie den Code mit dem PKCE-Verifier aus und prüft das ID-Token: seine Signatur, iss, aud, exp, iat und das nonce dieser Anmeldung. Die Redirect-URI wird aus APP_BASE_URL gebildet, niemals aus dem Host-Header:
const callbackUrl = new URL(config().redirectUri);
callbackUrl.search = new URL(request.url).search;
return client.authorizationCodeGrant(await keenauth(), callbackUrl, {
pkceCodeVerifier: transaction.codeVerifier,
expectedState: transaction.state,
expectedNonce: transaction.nonce,
idTokenExpected: true,
});
Eine fehlgeschlagene Prüfung antwortet mit 401 und einem festen Fehlercode. Nach einer erfolgreichen Anmeldung löscht die App die alte Sitzung und speichert Benutzer und Token in einer neuen:
// A new session id after sign-in prevents session fixation: the old session is deleted, a new one is created.
await sessions().destroySession(old);
const session = await sessions().getSession(null);
7. Seiten schützen und Ihre API aufrufen
Ein Loader, der einen Benutzer benötigt, ruft requireUser auf, das ohne Sitzung zur Startseite weiterleitet. Das Beispiel hat kein Refresh-Token: Wenn das Zugriffstoken abläuft, meldet sich der Benutzer erneut an.
export async function loader({ request }: Route.LoaderArgs) {
const { user, accessToken } = await requireUser(request);
const api = await fetchMessages(accessToken);
return { user, api };
}
Der Loader ruft Ihre API mit dem Zugriffstoken als Bearer-Token auf. Das Token bleibt auf dem Server; die Seite erhält nur den Benutzer und die Nachrichten.
response = await fetch(`${config().apiUrl}/api/messages`, {
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
});
8. Abmelden
Die Abmeldung ist ein POST von einer Seite dieser App. Die Action beendet die lokale Sitzung und schickt den Browser zum Logout-Endpunkt Ihrer Umgebung (OpenID Connect RP-Initiated Logout) mit id_token_hint, client_id und post_logout_redirect_uri, der ihn zurück zu Ihrer App leitet:
const session = await getSession(request);
const idToken = session.get("idToken");
const destroyed = await sessions().destroySession(session);
return redirect((await logoutUrl(idToken)).href, { headers: { "Set-Cookie": destroyed } });
9. Die App ausführen
Starten Sie die API aus der Express-Schnellstart-Anleitung (oder eine andere API, die diese Token akzeptiert) und führen Sie die App aus:
npm run dev
Öffnen Sie http://localhost:5173, wählen Sie Anmelden, melden Sie sich bei KeenAuth an und öffnen Sie das Dashboard.
10. Ohne Netzwerk testen
Die Tests benötigen weder ein Netzwerk noch ein KeenAuth-Konto. npm test baut die App und führt den Build über den Request-Handler von React Router aus. Ein gefälschtes fetch übernimmt die Rolle Ihrer Umgebung (Discovery, ein für den Testlauf erzeugter Schlüsselsatz, der Token-Endpunkt) und der API.
test("callback: an unknown or reused state is refused before any token request and starts no session", async () => {
const b = browser();
await startSignIn(b);
const wrong = await returnFromKeenAuth(b, { state: "not-the-state" });
npm test
npm run deps # exact pins, lockfile sources and licences
Die Tests prüfen, dass die Konfiguration validiert wird (und dass keine Fehlermeldung ein Geheimnis enthält). Sie prüfen, dass die Anmeldung PKCE S256, state, nonce und resource sendet und von einer anderen Seite oder mit einem GET abgelehnt wird. Sie prüfen, dass der Code mit dem passenden Verifier und mit Client-Authentifizierung eingetauscht wird, dass das Zugriffstoken im Authorization-Header an die API geht und nie an den Browser, und dass ein unbekannter oder wiederverwendeter state, ein fehlendes oder falsches iss, ein Autorisierungsfehler sowie ein ID-Token für einen anderen Client, mit einem anderen Nonce oder mit einem anderen Schlüssel signiert, ohne Sitzung abgelehnt werden. Sie prüfen, dass die Anmeldung eine neue Sitzungs-ID startet, dass eine Seite ohne Sitzung zur Startseite weiterleitet und dass die Abmeldung von einer anderen Seite abgelehnt wird und mit id_token_hint zum Logout-Endpunkt geht.
Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.
Hinter einem Proxy oder Load Balancer
Setzen Sie APP_BASE_URL auf die öffentliche URL der App. Die Redirect-URI und die Origin-Prüfung verwenden sie, niemals den Host-Header. Beendet ein Proxy TLS, bleibt APP_BASE_URL trotzdem die https-URL, sodass das Sitzungs-Cookie Secure bleibt.
Aktuelle Grenzen
- Kein Refresh-Token und keine stille Anmeldung: Der Benutzer meldet sich erneut an, wenn das Zugriffstoken abläuft.
- Der Dateisitzungsspeicher hält die Token als Klartext-Dateien unter
.sessions/und löscht abgelaufene Dateien nicht. Schützen Sie dieses Verzeichnis oder verwenden Sie einen gemeinsamen Speicher, der Sitzungen verschlüsselt und ablaufen lässt. - Der Client authentifiziert sich mit einem Client-Secret.
openid-clientunterstützt auchprivate_key_jwt; das Beispiel zeigt es nicht.
Nächste Schritte
- Eine Express-API schützen: die API, die diese App aufruft.
- Token prüfen: der Verifier unter der Middleware Ihrer API.