---
title: JWT-Authentifizierung
description: >-
  Schützen Sie Ihre Dokumentation mit Ihrem eigenen Login-System. Aktivieren Sie JWT in docs.json und signieren Sie kurzlebige Tokens für Benutzersitzungen.
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

<Note>
  JWT-Authentifizierung erfordert einen kostenpflichtigen Tarif und ein Jamdesk-Projekt, das [mit einem Git-Repository verbunden ist](/de/setup/connecting-github). Die Konfiguration befindet sich in `docs.json` und wird daher zusammen mit Ihrem normalen Build- und Bereitstellungsprozess verarbeitet.
</Note>

Wenn Ihr Produkt bereits über ein eigenes Login-System verfügt, können Sie mit der JWT-Authentifizierung Ihre Dokumentation darüber schützen, anstatt ein gemeinsames Passwort zu vergeben. Ihr Backend signiert ein kurzlebiges Token, wenn ein angemeldeter Benutzer über einen Link zur Dokumentation gelangt. Jamdesk überprüft es einmal, erstellt eine Sitzung und der Besucher kann anschließend normal navigieren. Besucher benötigen weder ein Jamdesk-Konto noch ein gemeinsames Passwort.

## Unterschiede zum Passwortschutz

[Passwortschutz](/de/setup/password-protection) gibt jedem Besucher dasselbe gemeinsame Passwort. Das eignet sich gut für interne Dokumentation, Staging-Vorschauen oder eine einzelne Partnerzielgruppe. Die JWT-Authentifizierung gilt pro Benutzer: Identität, Sitzungsdauer und Seitenzugriff jedes Besuchers werden durch ein Token bestimmt, das *Ihr* Backend signiert. Der Zugriff auf die Dokumentation kann Ihren bestehenden Kundenkonten, Tarifen oder Rollen folgen, anstatt auf einem gemeinsamen Geheimnis zu beruhen.

Die beiden Modi schließen sich gegenseitig aus: `auth.password` und `auth.jwt` können nicht gleichzeitig aktiviert sein. Wenn Sie von einem Modus zum anderen wechseln, lesen Sie unten [Von Passwortschutz migrieren](#von-passwortschutz-migrieren).

## Einrichtungsschritte

<Steps>
  <Step title="auth.jwt in docs.json aktivieren">
    ```json docs.json
    {
      "$schema": "https://jamdesk.com/docs.json",
      "name": "Acme Docs",
      "theme": "jam",
      "auth": {
        "jwt": {
          "enabled": true,
          "loginUrl": "https://app.example.com/docs-login",
          "public": ["/changelog/*"]
        }
      }
    }
    ```

    `loginUrl` ist erforderlich, sobald `enabled: true` gesetzt ist, und muss eine absolute `https://`-URL sein. Nicht authentifizierte Besucher werden mit `?redirect=<path>` dorthin weitergeleitet, damit Ihr Login-Ablauf weiß, wohin sie zurückkehren sollen. `public` ist optional: Pfade oder Globs (`*` für ein Segment, `**` für beliebige Tiefen), die ohne Anmeldung erreichbar bleiben.
  </Step>

  <Step title="Signaturschlüssel generieren">
    Öffnen Sie **Project Settings** im Dashboard und suchen Sie die Karte **JWT authentication**. Klicken Sie auf **Generate signing key**.

    Jamdesk erstellt ein Ed25519-Schlüsselpaar, behält nur den *öffentlichen* Schlüssel und zeigt Ihnen den *privaten* Schlüssel genau einmal an. Kopieren Sie ihn sofort in Ihren Secret Manager. Jamdesk speichert oder versendet den privaten Schlüssel niemals und kann ihn nicht wiederherstellen, wenn Sie ihn verlieren. Generieren Sie in diesem Fall einen neuen Schlüssel. Dadurch wird der alte ungültig. Aktualisieren Sie gleichzeitig den Signaturschlüssel Ihres Backends.
  </Step>

  <Step title="Committen und neu erstellen">
    ```bash
    git add docs.json
    git commit -m "Turn on JWT authentication"
    git push
    ```

    Sobald der Build veröffentlicht ist, schützt die Website jede Seite. Anfragen ohne gültige Sitzung werden zu Ihrer `loginUrl` weitergeleitet.
  </Step>
</Steps>

## Ihren Login-Ablauf integrieren

Wenn ein angemeldeter Benutzer über einen Link zu Ihrer Dokumentation gelangt, signiert Ihr Backend ein JWT und leitet den Browser mit dem Token im URL-Fragment (nach dem `#`) zur Callback-URL der Dokumentations-Website weiter. Fragmente gelangen niemals in Ihre Serverprotokolle oder zu einem Reverse Proxy, da Browser sie nicht zusammen mit der Anfrage senden.

Das Token muss mit EdDSA (Ed25519, passend zum im Dashboard generierten Schlüssel) signiert sein. Sein `exp`-Claim sollte höchstens etwa 10 Sekunden in der Zukunft liegen. Dies ist ein Zeitfenster für den Handshake, keine Sitzungsdauer. Die tatsächliche Sitzungsdauer wird separat durch das Feld `expiresAt` in der Payload gesteuert (siehe unten die [Payload-Referenz](#referenz-zur-payload)).

<CodeGroup>
```typescript TypeScript (jose)
import { SignJWT, importPKCS8 } from "jose";

// Store this in your secret manager. It's the private key Jamdesk showed
// you once when you generated it in Project Settings.
const privateKey = await importPKCS8(process.env.JAMDESK_JWT_PRIVATE_KEY!, "EdDSA");

async function signDocsToken(user: { groups: string[] }) {
  return new SignJWT({
    host: "acme.jamdesk.app", // or your custom domain, e.g. "docs.example.com"
    expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7-day session
    groups: user.groups,
    apiPlaygroundInputs: {
      header: { Authorization: `Bearer ${user.apiToken}` },
    },
  })
    .setProtectedHeader({ alg: "EdDSA" })
    .setExpirationTime("10s") // handshake window, not session length
    .sign(privateKey);
}

// In your "open docs" route/button handler:
app.get("/docs-login", requireAuth, async (req, res) => {
  const token = await signDocsToken(req.user);
  const redirect = req.query.redirect ?? "/";
  res.redirect(
    `https://acme.jamdesk.app/_jd/auth/callback?redirect=${encodeURIComponent(
      String(redirect)
    )}#${token}`
  );
});
```

```python Python (pyjwt)
import time
import jwt  # PyJWT >= 2.4, with the cryptography extra installed

with open("jamdesk_jwt_private_key.pem", "rb") as f:
    PRIVATE_KEY = f.read()

def sign_docs_token(user):
    payload = {
        "host": "acme.jamdesk.app",  # or your custom domain
        "exp": int(time.time()) + 10,  # handshake window, not session length
        "expiresAt": int(time.time()) + 60 * 60 * 24 * 7,  # 7-day session
        "groups": user.groups,
        "apiPlaygroundInputs": {
            "header": {"Authorization": f"Bearer {user.api_token}"},
        },
    }
    return jwt.encode(payload, PRIVATE_KEY, algorithm="EdDSA")

@app.route("/docs-login")
def docs_login():
    token = sign_docs_token(current_user)
    redirect_path = request.args.get("redirect", "/")
    return redirect(
        f"https://acme.jamdesk.app/_jd/auth/callback"
        f"?redirect={quote(redirect_path)}#{token}"
    )
```
</CodeGroup>

<Warning>
  Signieren Sie das Token ausschließlich serverseitig. Der private Schlüssel darf niemals einen Browser oder ein öffentliches Repository erreichen. Jeder, der ihn besitzt, kann Sitzungen für Ihre Dokumentations-Website erstellen.
</Warning>

## Weiterleitungsablauf

1. Ein Besucher ruft eine geschützte Seite (zum Beispiel `/quickstart`) ohne gültige Sitzung auf. Jamdesk antwortet mit einer Weiterleitung zu `{loginUrl}?redirect=%2Fquickstart`.
2. Ihr Login-Ablauf authentifiziert den Besucher (auf die für Sie übliche Weise), signiert ein JWT und leitet ihn zu `https://<your-docs-host>/_jd/auth/callback?redirect=%2Fquickstart#<jwt>` weiter.
3. Die Callback-Seite liest das Token clientseitig aus dem Fragment und sendet es an Jamdesks Token-Austausch-Endpoint. Jamdesk überprüft Signatur und Claims und setzt bei Erfolg ein signiertes Sitzungscookie.
4. Der Browser wird nun mit einer gültigen Sitzung zum ursprünglichen Ziel `/quickstart` weitergeleitet. Der Wert von `redirect` bleibt durchgehend erhalten, sodass Besucher genau dort landen, wo sie gestartet sind.

Wenn Ihr Backend keinen `redirect`-Wert bestimmen kann (beispielsweise weil jemand Ihre Login-Seite direkt als Lesezeichen aufgerufen hat), lassen Sie ihn weg. Jamdesk verwendet dann `/`.

## Öffentliche Seiten

Einige Seiten sollten ohne Anmeldung erreichbar bleiben, etwa eine Statusseite oder ein öffentliches Changelog. Es gibt drei Möglichkeiten, eine Seite als öffentlich zu kennzeichnen. Sie werden alle zu einer einzigen Allowlist zusammengeführt:

**Frontmatter**, jeweils für eine Seite:

```yaml
---
title: Changelog
public: true
---
```

**Navigationsgruppen**, für einen ganzen Abschnitt:

```json docs.json
{
  "navigation": {
    "groups": [
      { "group": "Changelog", "public": true, "pages": ["changelog"] }
    ]
  }
}
```

**Explizite Globs** unter `auth.jwt.public[]`:

```json docs.json
{
  "auth": {
    "jwt": {
      "enabled": true,
      "loginUrl": "https://app.example.com/docs-login",
      "public": ["/changelog/*", "/status"]
    }
  }
}
```

## Gruppenbasierter Zugriff

Einige Seiten sollen nur für bestimmte authentifizierte Benutzer sichtbar sein, etwa ein Runbook für Administratoren oder eine Referenz nur für Unternehmenskunden. Fügen Sie dem Frontmatter einer Seite `groups` hinzu:

```yaml
---
title: Admin API Keys
groups: ["admin"]
---
```

Die Sitzung eines Besuchers enthält das Array `groups`, das Ihr Backend in die JWT-Payload eingefügt hat. Wenn eine Seite `groups` definiert und sich die Sitzung des Besuchers nicht mit dieser Liste überschneidet, erhält der Besucher statt einer 401- oder einer Entsperrseite eine 404. Dies ist beabsichtigt: Eine gruppenbeschränkte Seite soll ihre eigene Existenz gegenüber Benutzern außerhalb der Gruppe nicht preisgeben.

Details zur Verwendung von `groups`:

- Gruppenseiten werden aus Sitemap, Suche, AI-Chat und MCP ausgeschlossen, auch für Benutzer, die der Gruppe angehören. Der Ausschluss von diesen Auffindbarkeitsflächen wird beim Build und nicht pro Besucher entschieden. Ein Mitglied der Gruppe `admin` kann `/admin/api-keys` weiterhin direkt öffnen (über URL oder internen Link), die Seite erscheint jedoch nicht in Suchergebnissen, Chat-Antworten oder `llms.txt`. Wenn eine eingeschränkte Seite für ihre eigene Zielgruppe auffindbar sein soll, verlinken Sie sie von einer anderen Seite, die diese Zielgruppe bereits erreichen kann.
- Ein leeres `groups: []` bedeutet keinerlei Einschränkung, nicht „niemand kann diese Seite sehen“. Um die Gruppenbeschränkung einer Seite zu entfernen, löschen Sie das Feld `groups` vollständig, anstatt ein leeres Array festzulegen.
- Um eine Seite für niemanden zugänglich zu machen, heben Sie ihre Veröffentlichung auf. Es gibt keinen `groups`-Wert für „niemand“: Die Gruppenmitgliedschaft ist additiv, und jede Überschneidung gewährt Zugriff.
- Lokalisierte Kopien übernehmen automatisch die `groups`-Angabe der Ausgangsseite, sofern die Übersetzung nicht ein eigenes `groups`-Feld in ihrem Frontmatter definiert. Durch die Übersetzung einer eingeschränkten Seite wird die Übersetzung nicht versehentlich öffentlich.
- Halten Sie Gruppennamen kurz. Gruppen werden im Sitzungscookie übertragen: bis zu 32 Gruppen pro Sitzung mit jeweils 64 Zeichen. Wird eines dieser Limits überschritten, wird die Liste nicht gekürzt. Jamdesk weist stattdessen das gesamte Token mit einer 401 zurück und gewährt keine Sitzung.

## API-Playground vorausfüllen

Wenn Ihre Dokumentation einen [API-Playground](/de/api-reference/playground) enthält, können Sie ihn für angemeldete Besucher vorausfüllen, damit sie ihren eigenen API-Schlüssel nicht einfügen müssen. Fügen Sie `apiPlaygroundInputs` in Ihre JWT-Payload ein:

```json
{
  "host": "acme.jamdesk.app",
  "apiPlaygroundInputs": {
    "header": { "Authorization": "Bearer sk_live_user_specific_token" },
    "query": { "org_id": "acme-corp" },
    "path": { "workspace_id": "ws_123" }
  }
}
```

- `header.Authorization` füllt das Authentifizierungsfeld des Playgrounds voraus. Ein Präfix `Bearer ` wird automatisch entfernt, falls vorhanden.
- `query` und `path` füllen passende Parameternamen des aktuellen Endpoints voraus.
- Die Bereiche `server` und `cookie` werden nicht unterstützt. Nur `header`, `query` und `path` werden angewendet.
- Beim Vorausfüllen wird kein Wert überschrieben, den der Besucher bereits in den Playground eingegeben hat.

## Referenz zur Payload

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `host` | Ja | Muss exakt mit dem Request-Host (ohne Berücksichtigung der Groß-/Kleinschreibung) übereinstimmen: Ihrer `*.jamdesk.app`-Subdomain oder Ihrer benutzerdefinierten Domain. Ein für einen Host signiertes Token wird bei jedem anderen Host abgewiesen. |
| `expiresAt` | Nein | Unix-Zeitstempel (Sekunden), der festlegt, wie lange die resultierende *Sitzung* gültig sein soll. Begrenzt auf 30 Tage; ohne Angabe gilt ein Standardwert von 7 Tagen. Dies ist unabhängig vom kurzlebigen `exp`-Claim des Tokens. |
| `groups` | Nein | Array von Gruppennamen, die die Sitzung enthalten soll, mit bis zu 32 Einträgen von jeweils 64 Zeichen. Bei Überschreitung eines der Limits wird das gesamte Token abgewiesen (401, keine Sitzung), anstatt die Liste zu kürzen. |
| `apiPlaygroundInputs` | Nein | Vorausfüllwerte für den API-Playground. Die serialisierte Größe ist auf 2 KB begrenzt. Wenn der Inhalt zu groß ist, wird er ohne Fehlermeldung verworfen und die Sitzung trotzdem gewährt. |

## Abmelden

Angemeldete Besucher sehen im Header der Dokumentation einen Link **Log out**. Dieser führt zu `/_jd/auth/logout`, löscht das Sitzungscookie und leitet zu Ihrer `loginUrl` weiter. Sie können auch direkt aus Ihrer eigenen App darauf verlinken, wenn Sie an anderer Stelle einen Link zum Abmelden aus der Dokumentation anbieten möchten. Es handelt sich um eine einfache `GET`-Anfrage, für die weder Body noch Header erforderlich sind.

## Verhalten von Funktionen bei aktivierter Authentifizierung

| Funktion | Verhalten |
|---|---|
| `llms.txt` / `llms-full.txt` / Sitemap | Wie der Rest der Website geschützt: Ohne gültige Sitzung nicht erreichbar, wie jede andere Seite. |
| Gruppeneingeschränkte Seiten | Aus allen oben genannten Artefakten sowie aus Suche und AI-Chat ausgeschlossen, unabhängig von den Gruppen der anfragenden Sitzung (siehe [Gruppenbasierter Zugriff](#gruppenbasierter-zugriff)). |
| `robots.txt` | Immer öffentlich. Suchmaschinen können sehen, dass eine Dokumentations-Website existiert und geschützt ist, aber nicht deren Inhalte. |

## Fehlerbehebung

<Accordion title="Ich habe den Signaturschlüssel rotiert, aber alte Sitzungen scheinen weiterhin zu funktionieren">
  Rotation und Widerruf werden innerhalb von etwa 15 Sekunden wirksam, nicht sofort, da das Edge-Gateway die Authentifizierungskonfiguration kurzzeitig zwischenspeichert, damit jede Seitenanfrage schnell bleibt. **Rotate** im Dashboard macht alle bestehenden Sitzungen ungültig. Warten Sie bis zu 15 Sekunden, bevor Sie eine weiterhin gültige alte Sitzung als Fehler behandeln.
</Accordion>

<Accordion title="Das Dashboard zeigt ein &quot;Runtime out of sync&quot;-Banner">
  Das bedeutet, dass Ihr neuester Signaturschlüssel den Runtime-Cache noch nicht erreicht hat. Ursache ist meist ein vorübergehender Schreibfehler, der die Schlüsselgenerierung oder -rotation unterbrochen hat. Jamdesk versucht die Synchronisierung automatisch erneut, sobald Sie die Einstellungsseite öffnen. Wenn das Banner bestehen bleibt, klicken Sie darauf auf **Retry sync**. Wenn es auch nach einem erneuten Versuch nicht verschwindet, rotieren Sie den Schlüssel über dieselbe Karte.
</Accordion>

<Accordion title="Besucher erhalten selbst mit einem sicher gültigen Token eine 401">
  Überprüfen Sie den `host`-Claim anhand des exakt angeforderten Hosts. Wenn Ihre Dokumentation sowohl unter einer benutzerdefinierten Domain (`docs.example.com`) als auch unter der zugrunde liegenden `*.jamdesk.app`-Subdomain erreichbar ist, wird ein für den einen Host signiertes Token beim anderen abgewiesen: Die Bindung an `host` ist exakt und unabhängig von der Groß-/Kleinschreibung, berücksichtigt aber keine Aliase. Signieren Sie Tokens für den Host, auf den Sie tatsächlich verlinken, oder signieren Sie zwei Varianten, wenn Sie auf beide verlinken.
</Accordion>

<Accordion title="Ich stecke in einer Weiterleitungsschleife zwischen meiner Login-Seite und der Dokumentations-Website fest">
  Jamdesks Callback-Route verweigert die Weiterleitung zurück zu sich selbst: Ein `redirect`-Wert, der auf `/_jd/auth/callback` (oder die darunterliegende Seite im Stil einer Entsperrseite) zeigt, wird zu `/` umgeschrieben, anstatt berücksichtigt zu werden. Wenn weiterhin eine Schleife auftritt, prüfen Sie, dass Ihr Login-Ablauf nicht selbst zyklisch zur `loginUrl` der Dokumentation weiterleitet (beispielsweise eine Login-Seite, die bei fehlender Dokumentationssitzung sofort zu `/docs-login` zurückspringt). Die Seite der Dokumentation ist gegen die Schleife geschützt; die Schleife liegt fast immer im Login-Ablauf.
</Accordion>

<Accordion title="auth.password und auth.jwt sind beide aktiviert">
  Dies ist ein `config_error` und blockiert den Build. Wählen Sie einen der beiden Modi. Lesen Sie [Von Passwortschutz migrieren](#von-passwortschutz-migrieren), um beim Wechsel die sichere Reihenfolge einzuhalten.
</Accordion>

## Sicherheitshinweis

`apiPlaygroundInputs`, einschließlich jedes darin abgelegten Authorization-Werts, kann von JavaScript gelesen werden, das auf Ihrer Dokumentations-Website über den Session-Info-Endpoint ausgeführt wird, der das Vorausfüllen des Playgrounds ermöglicht. Das Vorausfüllen ist praktisch, aber kein geeigneter Ort für hochprivilegierte Geheimnisse.

Senden Sie benutzerbezogene Zugangsdaten mit den geringstmöglichen Berechtigungen, die auf die erlaubten Aktionen des jeweiligen Besuchers beschränkt sind, niemals einen organisationsweiten Administratorschlüssel. Behandeln Sie alles, was Sie in `apiPlaygroundInputs` einfügen, als für die Person sichtbar, die die Dokumentation aufruft, denn genau das ist es.

## Von Passwortschutz migrieren

Der Wechsel von einem gemeinsamen Passwort zur JWT-Authentifizierung erfordert keine Ausfallzeit, und die Website bleibt durchgehend geschützt. Gehen Sie in dieser Reihenfolge vor:

<Steps>
  <Step title="JWT-Signaturschlüssel generieren">
    Führen Sie diesen Schritt zuerst aus, während der Passwortschutz noch aktiv ist. Das Generieren eines Schlüssels ändert nicht, was geschützt ist; das Passwort bleibt die ganze Zeit wirksam.
  </Step>
  <Step title="docs.json ändern und neu erstellen">
    ```json docs.json
    {
      "auth": {
        "password": { "enabled": false },
        "jwt": { "enabled": true, "loginUrl": "https://app.example.com/docs-login" }
      }
    }
    ```

    Committen und pushen Sie die Änderung. Sobald dieser Build veröffentlicht ist, wechselt der Schutz atomar von Passwort zu JWT, ohne ein Zeitfenster, in dem die Website ungeschützt wäre. Bestehende durch das Passwort freigeschaltete Sitzungen enden beim Wechsel. Besucher authentifizieren sich ab diesem Zeitpunkt über Ihren Login-Ablauf.
  </Step>
  <Step title="Passwort löschen">
    Sobald Sie bestätigt haben, dass der JWT-Ablauf durchgängig funktioniert, öffnen Sie wieder **Project Settings** und löschen Sie das gespeicherte Passwort. Es ist zu diesem Zeitpunkt wirkungslos (der Passwortmodus ist in `docs.json` deaktiviert), durch das Löschen wird jedoch der gespeicherte Hash vollständig entfernt.
  </Step>
</Steps>

## Wie geht es weiter?

<Columns cols={2}>
  <Card title="Access Control overview" icon="shield" href="/de/setup/access-control">
    Vergleichen Sie die JWT-Authentifizierung mit Passwortschutz, SSO und dem Muster mit mehreren Projekten.
  </Card>
  <Card title="Password Protection" icon="lock" href="/de/setup/password-protection">
    Die Alternative mit gemeinsamem Passwort: einfacher einzurichten, ohne erforderliche Backend-Integration.
  </Card>
  <Card title="SSO (Enterprise)" icon="key" href="/de/setup/sso">
    Die durch einen Identitätsanbieter gesteuerte Anmeldung für Unternehmenskunden.
  </Card>
  <Card title="Custom Domains" icon="globe" href="/de/deploy/custom-domains">
    Stellen Sie Ihre Dokumentation unter Ihrer eigenen Domain bereit, bevor Sie Ihren Login-Ablauf einrichten.
  </Card>
</Columns>