
# 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](/docs/quickstarts/react).

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](/docs/api/management/applications/create)) mit einem Client-Secret (`client_secret_basic`). Registrieren Sie in einer `development`-Umgebung die Redirect-URI `http://localhost/auth/callback` und die Post-Logout-Redirect-URI `http://localhost/`. Eine Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa `5173`.
- Eine API, die in der Umgebung registriert ist ([API erstellen](/docs/api/management/apis/create)), mit dem Scope `messages:read`. Ihr `identifier`, etwa `https://api.example.com`, ist die Audience ihrer Zugriffstoken. Die [Express-Schnellstart-Anleitung](/docs/quickstarts/express) 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

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

```ts
  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`):

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

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

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

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

```ts
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.

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

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

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

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

```ts
  // 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.

```ts
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.

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

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

```sh
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.

```ts
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" });
```

```sh
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-client` unterstützt auch `private_key_jwt`; das Beispiel zeigt es nicht.

## Nächste Schritte

- [Eine Express-API schützen](/docs/quickstarts/express): die API, die diese App aufruft.
- [Token prüfen](/docs/sdk/token-verification): der Verifier unter der Middleware Ihrer API.
