JWT-Guides

Wie ich JWTs in JavaScript korrekt dekodiere

Mein JavaScript- und Node.js-Ablauf für Bearer-Cleanup, Base64URL, UTF-8, JSON und exp, ohne Dekodierung mit Signaturprüfung zu verwechseln.

Von JWTDecoder.toolsZuletzt geprüft 2026-09-13

Redaktionelle Stimme: Formulierungen in der ersten Person beschreiben den dokumentierten Workflow von JWTDecoder.tools; Herausgeber und Organisationsautor ist JWTDecoder.tools.

JWT → Base64URL → UTF-8 → JSON
JWT → Base64URL → UTF-8 → JSON · decode ≠ verify

Wenn ich ein JWT in JavaScript dekodiere, setze ich zuerst eine klare Grenze: Dekodierung ist Inspektion, keine Authentifizierung. Ein erfolgreiches JSON.parse() macht das Token nicht vertrauenswürdig.

Für eine schnelle Sichtung nutze ich den JWT Decoder. Im eigenen Code gehe ich in sechs Schritten vor.

Meine sechs Schritte

  1. Bearer oder Authorization: Bearer entfernen.
  2. Segmentanzahl prüfen.
  3. Base64URL korrekt in Bytes umwandeln.
  4. Bytes als UTF-8 dekodieren.
  5. JSON mit getrennten Fehlern parsen.
  6. Claims wie exp lesen, aber Signature Verification separat halten.

1. Input normalisieren

function normalizeJwtInput(input) {
  return input
    .trim()
    .replace(/^authorization\s*:\s*bearer\s+/i, "")
    .replace(/^bearer\s+/i, "")
    .trim()
    .replace(/^['"]|['"]$/g, "");
}

Damit arbeitet der restliche Decoder immer mit dem compact token.

2. Segmente prüfen

function splitSignedJwt(input) {
  const token = normalizeJwtInput(input);
  const parts = token.split(".");

  if (parts.length === 5) throw new Error("Sieht nach einem verschlüsselten JWE aus");
  if (parts.length !== 3) throw new Error(`Erwartet: 3 Segmente, erhalten: ${parts.length}`);
  return parts;
}

Ich greife nicht blind auf token.split('.')[1] zu.

3. Base64URL bewusst behandeln

JWT verwendet URL-safe Alphabet und oft kein Padding.

function base64UrlToBytes(segment) {
  const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64.padEnd(
    base64.length + ((4 - (base64.length % 4)) % 4),
    "="
  );
  const binary = atob(padded);
  return Uint8Array.from(binary, c => c.charCodeAt(0));
}

atob() löst UTF-8 nicht automatisch.

4. UTF-8 vor JSON

function decodeJwtJson(segment, label) {
  let text;
  try {
    text = new TextDecoder("utf-8", { fatal: true })
      .decode(base64UrlToBytes(segment));
  } catch {
    throw new Error(`${label} ist kein gültiges Base64URL/UTF-8`);
  }

  let value;
  try {
    value = JSON.parse(text);
  } catch {
    throw new Error(`${label} ist kein gültiges JSON`);
  }

  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`${label} muss ein JSON-Objekt sein`);
  }
  return value;
}

ASCII-only Testdaten verbergen UTF-8-Probleme leicht. Darum nutze ich TextDecoder explizit.

5. Kleinen Decoder zusammensetzen

function decodeJwt(input) {
  const token = normalizeJwtInput(input);
  const parts = token.split(".");
  if (parts.length !== 3) throw new Error("3-teiliges signiertes JWT erwartet");

  return {
    token,
    header: decodeJwtJson(parts[0], "JWT header"),
    payload: decodeJwtJson(parts[1], "JWT payload"),
    signature: parts[2]
  };
}

Jetzt kann ich alg, kid, iss, aud, exp lesen. Die Signatur ist noch nicht verified.

6. exp: Sekunden gegen Millisekunden

function getExpiration(payload) {
  if (typeof payload.exp !== "number") return { hasExpiration: false };
  return {
    hasExpiration: true,
    expiresAt: new Date(payload.exp * 1000),
    expired: Date.now() >= payload.exp * 1000
  };
}

Falsch wäre:

Date.now() >= payload.exp

Für eine schnelle Timeline nutze ich den JWT Expiry Checker.

Node.js mit Buffer

function decodeBase64UrlJson(segment) {
  return JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
}

Für echte Server-Authentifizierung nutze ich trotzdem eine JWT/JOSE-Bibliothek, die Signature, Issuer, Audience und Algorithm Policy prüft.

jwt-decode ist ebenfalls nur Decode

import { jwtDecode } from "jwt-decode";

const payload = jwtDecode(token);
const header = jwtDecode(token, { header: true });

Die Library selbst macht klar, dass Dekodierung keine Validierung ist. Ich setze sie daher nicht als Authentication Gate ein.

Warum ich den Einzeiler vermeide

JSON.parse(atob(token.split(".")[1]));

Er:

  • setzt ein vorhandenes Payload-Segment voraus;
  • behandelt Base64URL nicht bewusst;
  • versteckt Padding-Probleme;
  • kann UTF-8 falsch behandeln;
  • liefert schwache Fehlerdiagnose;
  • ignoriert Header mit alg/kid;
  • verleitet dazu, Signature Verification zu vergessen.

Nach Decode folgen Verify und Validate

Für Authentizität gehe ich zu JWT Signature Verifier oder JWKS Validator.

Danach prüfe ich iss, aud, exp, nbf, Token-Typ und Permissions mit JWT Validator oder JWT Debugger.

Meine Browser-Regeln

  • JWT nicht in URLs schreiben.
  • Production Bearer Token nicht vollständig loggen.
  • Debug Tokens nicht unnötig persistent speichern.
  • Synthetische/redigierte Tokens bevorzugen.
  • Token nicht nur zum Base64URL-Decode an einen Server senden.

Meine JavaScript-Checkliste

  1. Bearer Prefix entfernen.
  2. Segmente prüfen.
  3. Base64URL → Bytes.
  4. UTF-8 dekodieren.
  5. Header/Payload sicher parsen.
  6. alg, kid, iss, aud, Zeit prüfen.
  7. NumericDate × 1000 für JS-Date.
  8. Ergebnis als decoded, nicht verified markieren.
  9. Bei Trust Signature mit bekanntem Key prüfen.
  10. Für Acceptance Issuer, Audience, Zeit, Typ und Permissions validieren.

Die wichtigste Engineering-Entscheidung ist für mich, readable, cryptographically verified und application-acceptable getrennt zu halten.

Quellen und Referenzen

Verwandte Guides