
# JavaScript-Browser-SDK

`@keenauth/js` ist der Browser-Kern von KeenAuth. Es meldet Benutzer in Ihrer Single-Page-App an, speichert deren Tokens und ruft Ihre API auf. Es hat keine Laufzeitabhängigkeiten, und die React-Komponenten bauen darauf auf. Das Paket ist noch nicht auf npm veröffentlicht.

## Anmeldung mit Weiterleitung

Das SDK ist ein Standard-Public-Client für OpenID Connect:

- Es verwendet PKCE (S256) und DPoP-gebundene Tokens.
- Es prüft `state`, `nonce` und `iss` im Callback.
- Es validiert die Signatur und die Claims des ID-Tokens anhand der Schlüssel Ihrer Umgebung.

```ts
import { KeenAuth } from "@keenauth/js";

const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  postLogoutRedirectUri: "https://app.acme.example/",
});

// On your sign-in button:
await auth.signInRedirect({ appState: "/orders" });

// On https://app.acme.example/callback:
const { appState } = await auth.handleRedirectCallback();
```

`handleRedirectCallback` akzeptiert eine Antwort nur einmal. Es schlägt mit einem `KeenAuthError` fehl, wenn:

- `state` oder `iss` nicht übereinstimmt;
- die ausstehende Anmeldung älter als 10 Minuten ist;
- das ID-Token nicht gültig ist.

Ein Fehler, den der Server zurückgibt, wird als `KeenAuthOAuthError` mit dem OAuth-Code `error` ausgelöst.

## Ihre API aufrufen

Um ein Zugriffstoken für Ihre API zu erhalten, übergeben Sie deren Kennung als `resource` (RFC 8707). Sie wird bei der Autorisierungsanfrage, beim Code-Austausch und bei jeder Aktualisierung gesendet, und der `aud` des Tokens ist diese Kennung:

```ts
const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  resource: "https://api.acme.example", // your API's identifier
  scope: "openid profile email offline_access messages:read",
});
```

- Der Wert muss ein absoluter URI ohne Fragment (`#`) sein. Er wird genau so gesendet, wie Sie ihn schreiben, verwenden Sie daher die Kennung der API so, wie sie registriert ist. Ein ungültiger Wert löst im Konstruktor einen `KeenAuthError` mit dem Code `config_invalid` aus.
- `signInRedirect({ resource })` überschreibt den Wert für eine einzelne Anmeldung.
- Ohne `resource` verwendet der Server die Standard-API der Anwendung, sofern eine festgelegt ist; andernfalls werden keine API-Scopes gewährt.
- Die Option akzeptiert auch ein Array, das wie RFC 8707 es erlaubt als wiederholte `resource`-Parameter gesendet wird. Ein KeenAuth-Zugriffstoken hat genau eine Audience, daher lehnt der Server mehr als einen Wert mit `invalid_target` ab (ein `KeenAuthOAuthError`).

```ts
const res = await auth.authorizedFetch("https://api.acme.example/v1/orders");

// Or take the token yourself; it is refreshed when it is about to expire.
const token = await auth.getAccessToken();
```

`authorizedFetch` sendet `Authorization: DPoP` mit einem frischen, an das Token gebundenen Nachweis. Wenn der Server einen DPoP-Nonce verlangt, wiederholt es den Aufruf einmal mit diesem Nonce. Gleichzeitige Aufrufe teilen sich eine Aktualisierungsanfrage, und jede Aktualisierung rotiert das Refresh-Token. Informationen zur Prüfung dieser Tokens in Ihrer API finden Sie unter [Tokens in Ihrer API prüfen](/docs/sdk/token-verification).

## Abmelden

```ts
await auth.signOut();
```

`signOut` verwirft die Tokens und widerruft das Refresh-Token. Anschließend leitet es den Browser zum Logout-Endpunkt Ihrer Umgebung weiter, der ihn zu `postLogoutRedirectUri` zurückbringt.

## Wo Tokens gespeichert werden

- Zugriffs-, Refresh- und ID-Tokens werden nur im Arbeitsspeicher gehalten, niemals in localStorage, sessionStorage, IndexedDB, Cookies oder der URL. Nach einem Neuladen der Seite wird der Benutzer per Weiterleitung erneut angemeldet; das SDK stellt die Sitzung nicht wieder her.
- sessionStorage enthält nur die einmaligen Werte einer laufenden Anmeldung.
- Der DPoP-Schlüssel ist ein nicht exportierbarer WebCrypto-Schlüssel, standardmäßig im Arbeitsspeicher gehalten.

## Eingebettete Anmeldung

Dasselbe Objekt stellt den eingebetteten Komponenten außerdem einen Frontend-API-Client zur Verfügung:

- `transactions` für die Schritte der Anmeldung;
- `me` für das Profil und die `user_metadata` des Kontos, E-Mail-Adressen (mit Bestätigungscode), Passkeys und Sitzungen;
- `webauthn` für Passkey-Zeremonien.

Diese Aufrufe verwenden die Cookies Ihrer Umgebung und einen CSRF-Header. Der Server akzeptiert sie nur vom eigenen Anmelde-Origin der Umgebung.

## Anmeldung ohne Weiterleitung

Starten Sie auf einer Seite außerhalb Ihres Anmelde-Origins die Transaktion mit Ihrer `clientId`. Das SDK bindet sie dann an seinen DPoP-Schlüssel statt an Cookies:

```ts
let tx = await auth.transactions.create({ client_id: "app_01j9...", organization: "acme" });
tx = await auth.transactions.step("identifier", { identifier: "ada@acme.example" });
tx = await auth.transactions.step("password", { password });
if (tx.state === "completed") await auth.complete(tx); // { kind: "signed_in" }
```

- Die Anwendung muss eine First-Party-Anwendung sein, und der Seiten-Origin muss einer ihrer erlaubten Web-Origins sein.
- Jeder Aufruf trägt einen DPoP-Nachweis und kein Cookie. Verlangt der Server einen DPoP-Nonce, wiederholt das SDK den Aufruf einmal mit diesem Nonce.
- Das SDK erstellt den PKCE-Verifier, `state` und `nonce` selbst und hält sie im Arbeitsspeicher. `scope` und `resource` stammen aus den Optionen; Sie können `organization` und `uiLocales` an `create` übergeben. Verwenden Sie hier genau einen `resource`-Wert.
- `complete` prüft die Antwort, tauscht den Autorisierungscode mit demselben DPoP-Schlüssel aus und hält die Tokens im Arbeitsspeicher. Die Seite navigiert nicht, und der Code wird nie an Ihren Code zurückgegeben.
- Jeder `step`-Aufruf, mit oder ohne `client_id`, sendet die Zeitzone des Browsers im Header `Idp-Zoneinfo`. Schließt der Schritt die Anmeldung ab und hat der Benutzer noch keine Zeitzone gewählt, speichert KeenAuth sie im Feld `zoneinfo` des Profils, wie es die gehostete Anmeldeseite tut. Meldet der Browser keine brauchbare Zone, wird der Header weggelassen und der Schritt bleibt unverändert.
- Benötigt die Anmeldung eine Seite, die der Benutzer sehen muss (Zustimmung, rechtliche Bedingungen, Auswahl einer Organisation), schlägt der letzte Schritt mit einem `KeenAuthProblemError` mit dem Code `authorization_failed` fehl. Verwenden Sie in diesem Fall `signInRedirect` für diesen Benutzer.

## Kontoaufrufe von Ihrem eigenen Origin

Sobald der Benutzer angemeldet ist und das SDK ein Refresh-Token hält (der Standard-Scope fordert `offline_access` an), benötigen die Aufrufe `me` und `organizations` kein Sitzungs-Cookie des Anmeldehosts mehr:

- Das SDK aktualisiert mit `resource=<issuer>/fapi` und erhält ein kurzlebiges Zugriffstoken für die Frontend-API. Es ist DPoP-gebunden und wie die anderen Tokens nur im Arbeitsspeicher gehalten.
- Jeder Kontoaufruf sendet `Authorization: DPoP <token>` und einen DPoP-Nachweis für diesen Aufruf, ohne Cookie. Verlangt der Server einen DPoP-Nonce, wiederholt das SDK den Aufruf einmal mit diesem Nonce; ist das Token nicht mehr gültig, holt es ein neues und wiederholt den Aufruf einmal.
- Das Refresh-Token rotiert bei jeder Aktualisierung, daher führt das SDK Aktualisierungen nacheinander aus.
- Dies funktioniert nur für eine First-Party-Anwendung, deren erlaubte Web-Origins den Origin Ihrer Seite enthalten.
- Ohne Tokens verwendet das SDK wie bisher das Sitzungs-Cookie.

Die Serverregeln finden Sie unter [Zugriffstoken für die Frontend-API](/docs/api/fapi/front-api-token).

## Organisation wechseln

`organizations.switch(orgId)` wechselt den angemeldeten Benutzer ohne Seitenaufruf in eine andere Organisation. Übergeben Sie die ID `org_…` einer Organisation aus `organizations.listMine()`:

```js
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
```

- Es benötigt dasselbe wie die oben genannten Kontoaufrufe: ein Refresh-Token, `redirectUri` und eine First-Party-Anwendung, deren erlaubte Web-Origins den Origin Ihrer Seite enthalten.
- Das SDK sendet eine [Wechselanfrage](/docs/api/fapi/transactions/create) mit dem Frontend-API-Token, tauscht den Code mit einem Nachweis desselben Schlüssels aus und behält die neuen Tokens, deren `org_id` die Organisation ist. Die bisher gehaltenen Tokens bleiben erhalten, bis all das erfolgreich abgeschlossen ist.
- Benötigt die Organisation eine Seite, die der Benutzer sehen muss (MFA, SSO, Zustimmung), schlägt der Aufruf mit einem `KeenAuthProblemError` mit dem Code `authorization_failed` fehl. Verwenden Sie in diesem Fall `signInRedirect({ organization })` für diesen Benutzer.

## Aktuelle Einschränkungen

- Profil-Schreibvorgänge (`me.update`, `me.updateMetadata`) senden `If-Match` mit dem `ETag` des letzten Profil-Lese- oder Schreibvorgangs. Wurde das Profil zwischenzeitlich anderweitig geändert, schlägt der Schreibvorgang mit einem `KeenAuthProblemError` mit dem Code `precondition_failed` fehl; rufen Sie `me.get()` erneut auf, bevor Sie es wiederholen.
- Telefonnummern sind noch nicht verfügbar: `me.phones` ist `undefined`.
