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

1. Installieren

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:

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

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:

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.

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

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:

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:

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:

      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

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

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.

  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");
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