
# Anmeldung zu einer React-Single-Page-App hinzufügen

Dieser Quickstart fügt einer mit Vite erstellten React-Single-Page-App die KeenAuth-Anmeldung hinzu und verwendet dafür das Browser-SDK `@keenauth/js`. Die App meldet Benutzer per Weiterleitung an, ruft eine geschützte API mit einem DPoP-gebundenen Zugriffstoken auf und meldet Benutzer wieder ab. Sie testen sie außerdem offline. Das SDK wird in [JavaScript-Browser-SDK](/docs/sdk/js) beschrieben.

In einer Single-Page-App liegen die Tokens im Browser. Das SDK hält sie nur im Speicher und bindet sie mit DPoP an einen nicht exportierbaren Schlüssel, sodass ein gestohlenes Token ohne diesen Schlüssel nicht verwendet werden kann. Wenn Ihre App einen Server hat, bewahrt der [Next.js-Quickstart](/docs/quickstarts/nextjs) alle Tokens stattdessen auf dem Server auf (Backend for Frontend), was die stärkere Wahl ist.

Die App hat folgende Pfade:

| Pfad | Was er tut |
|---|---|
| `/` | Eine Schaltfläche zum Anmelden bzw. der Benutzer, eine Schaltfläche, die Ihre API aufruft, und eine Schaltfläche zum Abmelden |
| `/callback` | Die Redirect-URI. Das SDK schließt die Anmeldung hier ab, und die App kehrt zu der Seite zurück, auf der der Benutzer begonnen hat |

## Voraussetzungen

- Eine KeenAuth-Umgebung. Ihr Issuer sieht etwa so aus: `https://your-environment.eu.keenauth.app`.
- Eine Single-Page-Anwendung in der Umgebung ([Anwendung anlegen](/docs/api/management/applications/create)) mit `app_type` `spa` und `token_endpoint_auth_method` `none` (ein Public Client hat kein Secret). In einer `development`-Umgebung registrieren Sie:
  - die Redirect-URI `http://localhost/callback` (eine Loopback-Redirect-URI wird ohne Port registriert und passt auf jeden Port);
  - die Post-Logout-Redirect-URI `http://localhost/`;
  - den erlaubten Web-Origin `http://localhost:5173`, damit der Browser den Token-Endpunkt aufrufen darf.
- Eine API, die in der Umgebung registriert ist ([API anlegen](/docs/api/management/apis/create)) mit dem Scope `messages:read`. Ihr `identifier`, zum Beispiel `https://api.example.com`, ist die Audience ihrer Zugriffstoken. Der [Express-Quickstart](/docs/quickstarts/express) baut eine API, die diese Tokens akzeptiert.
- Node.js 22.18 oder neuer.

## 1. Installieren

```sh
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @keenauth/js
```

Das SDK ist noch nicht veröffentlicht. Bis dahin verlinkt das Beispiel auf den SDK-Quellcode in seiner `package.json`. Das Beispiel fixiert `vite@8.3.4`, `@vitejs/plugin-react@6.1.2`, `react@19.3.1` und `react-dom@19.3.1`.

## 2. Konfigurieren

Die App liest ihre Konfiguration ausschließlich aus Umgebungsvariablen. Legen Sie sie in `.env.local` ab, die Vite lädt, und committen Sie diese Datei niemals. Vite bettet jede `VITE_`-Variable in das JavaScript ein, das der Browser herunterlädt. Hinterlegen Sie dort daher kein Secret. Diese App hat keins.

| Variable | Pflicht | Bedeutung |
|---|---|---|
| `VITE_KEENAUTH_ISSUER` | ja | Der Issuer Ihrer Umgebung |
| `VITE_KEENAUTH_CLIENT_ID` | ja | Die `client_id` der Single-Page-Anwendung |
| `VITE_KEENAUTH_API_AUDIENCE` | ja | Der Identifier Ihrer API in KeenAuth |
| `VITE_API_URL` | nein | Basis-URL Ihrer API. Leer bedeutet der eigene Origin der App |
| `API_PROXY_TARGET` | nein | Nur für die Entwicklung: Ziel, an das der Dev-Server `/api` weiterleitet; Standard `http://localhost:8080` |

`loadConfig` liest diese Werte und prüft sie. Fehlt einer, bricht es die App mit einem Fehler ab, der die Variable nennt:

```ts
export function loadConfig(env: Readonly<Record<string, unknown>>): AppConfig {
  const need = (name: string): string => {
    const v = env[name];
    if (typeof v !== "string" || v.trim() === "") throw new Error(`${name} is required (see .env.example)`);
    return v.trim();
  };
```

## 3. SDK-Client erstellen

```ts
export function createAuth(config: AppConfig, origin: string, inject: Injectables = {}): KeenAuth {
  return new KeenAuth({
    issuer: config.issuer,
    clientId: config.clientId,
    redirectUri: `${origin}${CALLBACK_PATH}`,
    postLogoutRedirectUri: `${origin}/`,
    // The access token is for this API: its aud is the API identifier (RFC 8707).
    resource: config.apiAudience,
    // messages:read is a scope of that API; the example API requires it on /api/messages.
    scope: "openid profile email offline_access messages:read",
    ...inject,
  });
}
```

`resource` ist der Identifier Ihrer API (RFC 8707). Das SDK sendet ihn bei der Autorisierungsanfrage, beim Austausch des Codes und bei jedem Refresh, und die Audience des Zugriffstokens ist Ihre API. Ohne ihn ist das Zugriffstoken nicht für Ihre API bestimmt, und die API weist es zurück. Das SDK prüft jede Option beim Erstellen des Clients. Ein ungültiger Issuer oder `resource` löst einen `KeenAuthError` mit dem Code `config_invalid` aus.

Erstellen Sie einen Client für die Seite und übergeben Sie ihn an einen Provider:

```tsx
const config = loadConfig(import.meta.env);
const auth = createAuth(config, window.location.origin);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider auth={auth}>
      <App apiUrl={config.apiUrl} />
    </AuthProvider>
  </StrictMode>,
);
```

## 4. Anmelden und den Callback verarbeiten

Die Anmeldeschaltfläche startet die Anmeldung per Weiterleitung: Authorization-Code-Flow mit PKCE (S256), `state` und `nonce`. `appState` ist der Pfad, zu dem zurückgekehrt wird.

```tsx
        <button onClick={() => auth.signInRedirect({ appState: window.location.pathname })}>Sign in</button>
```

Wenn KeenAuth zu `/callback` zurückleitet, schließt der Provider die Anmeldung ab. Das SDK prüft `state`, `iss` und das ID-Token und tauscht den Code mit einem DPoP-Proof aus. Eine Autorisierungsantwort kann nur einmal verwendet werden, und Reacts `StrictMode` führt Effekte in der Entwicklung zweimal aus. Deshalb merkt sich `completeSignIn` das erste Ergebnis:

```ts
export function completeSignIn(auth: KeenAuth, href: string): Promise<string | null> {
  const url = new URL(href);
  if (url.pathname !== CALLBACK_PATH || !(url.searchParams.has("code") || url.searchParams.has("error"))) {
    return Promise.resolve(null);
  }
  let done = completions.get(auth);
  if (!done) {
    done = auth.handleRedirectCallback(href).then((r) => safeReturnPath(r.appState));
    completions.set(auth, done);
  }
  return done;
}
```

Anschließend entfernt der Provider `code` und `state` aus der Adresszeile. Als Rückkehrpfad wird nur ein Pfad innerhalb Ihrer App verwendet, damit ein manipuliertes `appState` den Benutzer nicht auf eine andere Website schicken kann:

```ts
export function safeReturnPath(path: string | undefined): string {
  return path !== undefined && /^\/(?![/\\])/.test(path) ? path : "/";
}
```

Liefern Sie `index.html` für `/callback` auch in der Produktion aus (der Vite-Dev-Server tut dies bereits).

## 5. Ihre API aufrufen

`authorizedFetch` sendet `Authorization: DPoP <token>` mit einem frischen, an das Token gebundenen Proof und erneuert das Token, bevor es abläuft:

```ts
export async function fetchMessages(auth: KeenAuth, apiUrl: string): Promise<unknown> {
  const res = await auth.authorizedFetch(`${apiUrl}/api/messages`, { headers: { Accept: "application/json" } });
  if (!res.ok) throw new Error(`The API answered ${res.status}`);
  return res.json();
}
```

In der Entwicklung leitet der Vite-Dev-Server `/api` an Ihre API weiter. Der Browser ruft die API somit auf dem eigenen Origin der App auf, und die API benötigt kein CORS:

```ts
      proxy: { "/api": env.API_PROXY_TARGET || "http://localhost:8080" },
```

Die API prüft den DPoP-Proof gegen die URL, die der Browser aufgerufen hat. Wenn Sie die API des Express-Quickstarts hinter diesem Proxy betreiben, setzen Sie ihre `PUBLIC_BASE_URL` auf `http://localhost:5173`. In der Produktion stellen Sie die API auf demselben Origin bereit oder aktivieren CORS dafür für den Origin Ihrer App und setzen `VITE_API_URL`.

## 6. Abmelden

```tsx
      <button onClick={() => auth.signOut()}>Sign out</button>
```

`signOut` verwirft die Tokens, widerruft das Refresh-Token und schickt den Browser zum Logout-Endpunkt Ihrer Umgebung, der ihn zu `/` zurückleitet.

## 7. Die App starten

Starten Sie die API aus dem Express-Quickstart mit `PUBLIC_BASE_URL=http://localhost:5173` und `KEENAUTH_AUDIENCE` auf den Identifier Ihrer API gesetzt, und starten Sie dann die App:

```sh
cp .env.example .env.local   # then fill it in
npm run dev
```

Öffnen Sie `http://localhost:5173`, wählen Sie **Sign in**, melden Sie sich bei KeenAuth an und wählen Sie **Call the API**.

Das SDK behält Tokens nur im Speicher. Nach einem Neuladen der Seite ist der Benutzer in der App abgemeldet und meldet sich erneut an. Die KeenAuth-Sitzung sorgt meist dafür, dass dies eine Weiterleitung ohne Abfrage ist.

## 8. Ohne Netzwerk testen

Die Tests benötigen weder ein Netzwerk noch ein KeenAuth-Konto. Ein gefälschtes `fetch` übernimmt die Rolle des Autorisierungsservers und der API, und das ID-Token wird mit einem eigens für den Test erzeugten Schlüssel signiert. Node führt die TypeScript-Tests direkt aus.

```ts
  assert.deepEqual(await fetchMessages(s.auth, ""), { messages: ["hello"] });
  const call = s.seen.find((c) => c.url === `${ORIGIN}/api/messages`)!;
  assert.equal(call.headers.get("Authorization"), "DPoP access-token-1");
```

```sh
npm test
npm run typecheck
npm run deps    # pinned versions, lockfile sources and licences
npm run build
```

Die Tests prüfen, dass die Konfiguration validiert wird, dass das SDK einen ungültigen Issuer oder `resource` ablehnt, dass die Anmeldung PKCE S256 und Ihre API als `resource` sendet, dass der Callback genau einmal abgeschlossen wird und `resource` zusammen mit dem Code sendet, dass ein gefälschtes `state` vor jeder Token-Anfrage abgelehnt wird, dass nach der Anmeldung nur ein Pfad innerhalb der App verwendet wird, dass kein Token in den Speicher geschrieben wird und dass der API-Aufruf das DPoP-gebundene Token und seinen Proof enthält.

Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.

## Nächste Schritte

- [JavaScript-Browser-SDK](/docs/sdk/js): alle Optionen, Speicherort der Tokens und die aktuellen Einschränkungen.
- [React-SDK](/docs/sdk/react): eingebettete Komponenten für Anmeldung, Registrierung und Konto.
- [Express-API absichern](/docs/quickstarts/express): die API, die diese App aufruft.
