Datei hochladen
  1. base64hub
  2. Base64 verstehen
  3. Base64 in JWT
Erklärung

Wie JWT Base64URL nutzt: ein Token von Hand dekodieren

Ein JWT besteht aus drei Base64URL-Strings, verbunden durch Punkte. Die ersten beiden kann jeder dekodieren, geschützt werden sie nur durch die Signatur. So ist jeder Teil aufgebaut und so lesen Sie ihn.

Aufbau eines Tokens

Hier ein vollständiges HS256-Token, signiert mit dem Demo-Secret demo-secret-do-not-use. Die Zeilenumbrüche dienen nur der Lesbarkeit, ein echtes Token ist eine einzige Zeile:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSIsImFkbWluIjpmYWxzZSwiaWF0IjoxNzYwMDAwMDAwLCJleHAiOjE3NjAwMDM2MDB9
.nKTVXOM_r1JbE92WsFd2SSun0a1TByUV8ZpqrRUnIhk

Die drei Teile, getrennt durch .:

TeilLänge in Base64URLDekodiert
Header36{"alg":"HS256","typ":"JWT"}
Payload104{"sub":"user-42","name":"Ada","admin":false,"iat":1760000000,"exp":1760003600}
Signatur4332 rohe Bytes HMAC-SHA256

Header und Payload sind JSON, kodiert als UTF-8 und dann als Base64URL. Die Signatur ist binär und ergibt nur als Bytes Sinn. iat und exp sind Unix-Zeitstempel in Sekunden: Dieses Token wurde am 2025-10-09 um 08:53:20 UTC ausgestellt und lief eine Stunde später ab.

Warum Base64URL ohne Padding

Ein Token reist an Orte, an denen +, / und = Ärger machen: Query-Strings (+ wird zum Leerzeichen), URL-Pfade (/ ist ein Trenner), Cookies und HTTP-Header. Deshalb verwendet JWS, das Signaturformat hinter JWT, definiert in RFC 7515, das URL-sichere Alphabet aus RFC 4648 und entfernt das Padding:

Base64-Kodierung mit dem in Abschnitt 5 von RFC 4648 definierten URL- und dateinamensicheren Zeichensatz, unter Weglassung aller abschließenden „=“-Zeichen (wie in Abschnitt 3.2 erlaubt) und ohne Zeilenumbrüche, Leerraum oder andere zusätzliche Zeichen.

Daraus ergeben sich zwei Änderungen gegenüber Standard-Base64:

  • + wird zu - und / wird zu _. Die Signatur oben enthält _, was Standard-Decoder ablehnen.
  • Kein = am Ende. Die Länge eines Segments ist nie 4n+1, ein Decoder kann also immer ermitteln, wie viel Padding entfernt wurde: Länge mod 4 gleich 2 bedeutet ==, 3 bedeutet =.

Mehr zum Unterschied der Alphabete unter Base64 vs. Base64URL.

Die Teile von Hand dekodieren

An den Punkten trennen, Standardalphabet und Padding wiederherstellen, dekodieren, dann JSON parsen. Im Browser:

function decodePart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
  const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const [header, payload] = token.split(".").slice(0, 2).map(decodePart);

Ohne den Schritt mit TextDecoder geht Nicht-ASCII-Text kaputt. Eine Payload mit {"name":"Zoë 日本"} wird zu eyJuYW1lIjoiWm_DqyDml6XmnKwifQ kodiert. Schickt man den umgewandelten String nur durch atob, kommt der Name als Zeichensalat heraus, der mit Zoë beginnt, weil atob ein Zeichen pro Byte liefert. In Laufzeitumgebungen, die es haben, ersetzt Uint8Array.fromBase64(part, { alphabet: "base64url" }) die ersten drei Zeilen, siehe Uint8Array.toBase64. In Node.js erledigt Buffer.from(part, "base64url") dasselbe.

In Python kümmert sich urlsafe_b64decode um das Alphabet, will aber weiterhin Padding:

import base64, json

def b64url_decode(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

header, payload, signature = token.split(".")
print(json.loads(b64url_decode(header)))   # {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(b64url_decode(payload)))  # {'sub': 'user-42', 'name': 'Ada', ...}

In der Shell übersetzen Sie das Alphabet mit tr und füllen in einer Schleife auf:

jwt_part() {
  local s
  s=$(printf '%s' "$1" | tr '_-' '/+')
  while [ $(( ${#s} % 4 )) -ne 0 ]; do s="$s="; done
  printf '%s' "$s" | base64 -d
}
jwt_part "$(echo "$TOKEN" | cut -d. -f2)"

Wenn Sie nur in ein Token hineinsehen wollen, fügen Sie es in den JWT-Decoder ein. Er läuft in Ihrem Browser, ein Produktions-Token verlässt Ihren Rechner also nicht.

Warum die Payload lesbar ist

Kodierung ist keine Verschlüsselung. Base64URL ändert nur, wie die Bytes geschrieben werden, und der JWT-Decoder kehrt das ohne jeden Schlüssel um. Ein signiertes JWT garantiert, dass die Payload seit dem Signieren nicht verändert wurde. Es verbirgt die Payload nicht.

Die Kodierung kostet außerdem Platz. Jeder Claim wächst durch Base64URL um ein Drittel, und das ganze Token reist mit jeder Anfrage in einem Header oder Cookie mit. Halten Sie die Payload also klein: eine ID und ein paar kurze Claims statt eines vollständigen Benutzerprofils.

Behandeln Sie jeden Claim als öffentlich. Benutzer-IDs, Rollen und Ablaufzeiten sind in Ordnung. Passwörter, API-Schlüssel und personenbezogene Daten, die der Token-Inhaber nicht sehen soll, sind es nicht. Muss der Inhalt vertraulich sein, verwenden Sie ein verschlüsseltes JWT (JWE). Seine kompakte Form hat fünf Base64URL-Teile: Header, verschlüsselter Schlüssel, IV, Chiffretext und Authentifizierungs-Tag. Den Unterschied allgemein erklärt Kodierung vs. Verschlüsselung.

Wie die Signatur berechnet wird

Die Signatur deckt exakt den ASCII-Text header.payload ab, die beiden Base64URL-Strings mit dem Punkt dazwischen. Für HS256:

import hmac, hashlib

signing_input = f"{header}.{payload}".encode("ascii")
mac = hmac.new(b"demo-secret-do-not-use", signing_input, hashlib.sha256).digest()
hmac.compare_digest(mac, b64url_decode(signature))   # True

Die rohen Signatur-Bytes werden dann ohne Padding in Base64URL kodiert. Ihre Länge hängt vom Algorithmus ab:

AlgorithmusSignatur-BytesBase64URL-Zeichen
HS2563243
ES2566486
RS256 (2048-Bit-Schlüssel)256342

ES256-Signaturen in JWS sind die rohen Werte R || S mit je 32 Bytes. OpenSSL und viele Krypto-Bibliotheken erzeugen stattdessen eine DER-kodierte ECDSA-Signatur, die mit 30 beginnt und eine variable Länge hat. Die Umwandlung zwischen beiden ist eine häufige Quelle von Fehlern der Art „invalid signature“.

Um ein Token mit HS-, RS-, ES- oder EdDSA-Schlüsseln zu bauen und zu signieren, verwenden Sie den JWT-Encoder. Für ein rohes HMAC in Base64 gibt es das HMAC-Tool.

Häufige Fehler

  • atob oder einen Standard-Base64-Decoder direkt verwenden. Das funktioniert bei manchen Tokens und wirft bei anderen einen Fehler, je nachdem, ob zufällig ein - oder _ vorkommt. Unsere Beispiel-Payload lässt sich mit atob dekodieren, die Signatur nicht.
  • Beim Erstellen von Tokens Padding anhängen. Manche Bibliotheken lehnen Segmente ab, die mit = enden. Entfernen Sie es.
  • Header oder Payload vor dem Prüfen neu kodieren. Die Signatur gilt für den ursprünglichen Base64URL-Text. Wer das JSON parst und neu serialisiert, kann Schlüsselreihenfolge oder Leerraum verändern, und die Signatur passt nicht mehr. Prüfen Sie immer gegen die Segmente, wie sie empfangen wurden.
  • Einer dekodierten Payload vertrauen. Dekodieren ist kein Prüfen. Kontrollieren Sie die Signatur mit dem erwarteten Algorithmus und akzeptieren Sie nie "alg":"none" (Header eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0), es sei denn, Sie wollen es so.
  • Den Token-String als eindeutigen Schlüssel verwenden. 32 Signatur-Bytes brauchen 256 Bit, und 43 Zeichen tragen 258. Die letzten 2 Bit sind ungenutzt, in unserem Beispiel lässt sich das abschließende k also durch l, m oder n ersetzen, und tolerante Decoder liefern trotzdem dieselben Bytes. Die Signatur ist weiterhin gültig. Für die Sicherheit ist das harmlos, aber zwei verschiedene Strings können dasselbe Token darstellen.
  • Zeitstempel als Millisekunden lesen. exp und iat sind Sekunden. Multiplizieren Sie mit 1000, bevor Sie sie an Date in JavaScript übergeben.

Häufige Fragen

Ist die Payload eines JWT verschlüsselt?

Nein. Ein normales signiertes JWT (JWS) kodiert die Payload nur in Base64URL, jeder, der das Token hat, kann sie also lesen. Verschlüsselte Tokens verwenden JWE, das fünf statt drei Teile hat.

Warum endet ein JWT nicht mit =-Zeichen?

RFC 7515 definiert die Kodierung als Base64URL ohne alle abschließenden =, damit Tokens ohne Escaping in URLs und Header passen. Decoder müssen das Padding wieder ergänzen oder Eingaben ohne Padding akzeptieren.

Lassen sich die drei Teile ohne den geheimen Schlüssel dekodieren?

Ja. Header und Payload sind einfaches Base64URL. Den Schlüssel braucht man nur, um die Signatur zu prüfen oder eine neue zu erstellen. Der JWT-Decoder zeigt beide Teile und prüft die Signatur, wenn Sie den Schlüssel angeben.

Warum beginnt jedes JWT mit eyJ?

Der Header ist ein JSON-Objekt, das mit {" beginnt, und diese zwei Bytes werden immer zu eyJ kodiert. Weitere solche Präfixe stehen unter Häufige Base64-Präfixe.

Weiterlesen