DevToolBoxKOSTENLOS
Blog

JWT Token ohne Bibliothek dekodieren: JavaScript, Python, Bash One-Liner

6 Min. Lesezeitvon DevToolBox

Every JWT is just three Base64URL-encoded strings separated by dots. You do not need jsonwebtoken, PyJWT, or any library to read what is inside. This guide gives you working one-liners in JavaScript, Node.js, Python, Bash, Go, and PHP so you can decode any JWT token instantly from any environment.

1. JWT Structure: header.payload.signature

A JWT consists of exactly three parts separated by periods. The first two parts (header and payload) are Base64URL-encoded JSON objects. The third part is the cryptographic signature. You only need to decode the first two parts to read the token contents.

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              JWT Token Layout                                   │
├──────────────────┬──────────────────────────────────┬──────────────────────────┤
│     HEADER       │           PAYLOAD                │       SIGNATURE          │
│  (algorithm)     │        (your data)               │    (verification)        │
├──────────────────┼──────────────────────────────────┼──────────────────────────┤
│ eyJhbGciOiJIUz   │ eyJzdWIiOiIxMjM0NTY3ODkwIiwi   │ SflKxwRJSMeKKF2QT4fw    │
│ I1NiIsInR5cCI6   │ bmFtZSI6IkpvaG4gRG9lIiwiaWF0   │ pMeJf36POk6yJV_adQss    │
│ IkpXVCJ9         │ IjoxNTE2MjM5MDIyfQ              │ w5c                      │
├──────────────────┼──────────────────────────────────┼──────────────────────────┤
│ Base64URL({      │ Base64URL({                      │ HMACSHA256(              │
│   "alg": "HS256" │   "sub": "1234567890",           │   header + "." +         │
│   "typ": "JWT"   │   "name": "John Doe",            │   payload,               │
│ })               │   "iat": 1516239022              │   secret                 │
│                  │ })                               │ )                        │
└──────────────────┴──────────────────────────────────┴──────────────────────────┘

Full token (dots separate the three parts):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2. JavaScript One-Liner (Browser)

The browser provides atob() to decode Base64 strings. Combined with JSON.parse(), you get a single expression that decodes any JWT payload. Note that atob() expects standard Base64, so we must replace the URL-safe characters first.

// Decode JWT payload in the browser — one line
JSON.parse(atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')))

// Result: { sub: "1234567890", name: "John Doe", iat: 1516239022 }

// Reusable function (handles UTF-8 payloads too):
const jwtDecode = (t) => JSON.parse(decodeURIComponent(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')));

// Decode the header:
JSON.parse(atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[0].replace(/-/g,'+').replace(/_/g,'/')))

3. Node.js One-Liner

Node.js does not have atob() in older versions, but Buffer.from() handles Base64URL natively since Node 15.7+. This is the cleanest approach for server-side JavaScript.

// Node.js — decode JWT payload in one line
JSON.parse(Buffer.from('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[1], 'base64url').toString())

// Result: { sub: '1234567890', name: 'John Doe', iat: 1516239022 }

// Decode header:
JSON.parse(Buffer.from('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[0], 'base64url').toString())

// For Node < 15.7 (no 'base64url' encoding):
JSON.parse(Buffer.from('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'), 'base64').toString())

4. Python One-Liner

Python's base64 module has urlsafe_b64decode which handles the URL-safe alphabet directly. The only catch is that you need to add padding, because JWT strips trailing = characters.

# Python — decode JWT payload in one line
import json, base64; print(json.loads(base64.urlsafe_b64decode('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[1] + '==')))

# Result: {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022}

# Proper padding (handles any token length):
import json, base64; p = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[1]; print(json.loads(base64.urlsafe_b64decode(p + '=' * (-len(p) % 4))))

# Decode header:
import json, base64; h = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'.split('.')[0]; print(json.loads(base64.urlsafe_b64decode(h + '=' * (-len(h) % 4))))

# Result: {'alg': 'HS256', 'typ': 'JWT'}

5. Bash One-Liner

Using standard Unix tools (cut, base64, jq), you can decode JWTs directly in your terminal. This is invaluable for debugging in CI/CD pipelines, Docker containers, and SSH sessions.

# Bash — decode JWT payload with base64 and jq
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Output:
# {
#   "sub": "1234567890",
#   "name": "John Doe",
#   "iat": 1516239022
# }

# macOS (uses -D flag for base64 decode):
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' | cut -d. -f2 | base64 -D 2>/dev/null | jq .

# Decode header (change -f2 to -f1):
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' | cut -d. -f1 | base64 -d 2>/dev/null | jq .

# Handle padding automatically (works on both Linux and macOS):
jwt='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; p=$(echo "$jwt" | cut -d. -f2); echo "$p"$(printf '%0.s=' $(seq 1 $((4 - ${#p} % 4)))) | base64 -d 2>/dev/null | jq .

# Without jq (Python fallback):
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

6. Go One-Liner (Compact Function)

Go's standard library includes encoding/base64 with RawURLEncoding which handles Base64URL without padding. No third-party packages needed.

package main

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "strings"
)

func decodeJWT(token string) (header, payload map[string]interface{}, err error) {
    parts := strings.Split(token, ".")
    if len(parts) != 3 {
        return nil, nil, fmt.Errorf("invalid JWT: expected 3 parts, got %d", len(parts))
    }
    // Decode header
    hBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
    if err != nil { return nil, nil, err }
    json.Unmarshal(hBytes, &header)
    // Decode payload
    pBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
    if err != nil { return nil, nil, err }
    json.Unmarshal(pBytes, &payload)
    return header, payload, nil
}

func main() {
    token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    _, payload, _ := decodeJWT(token)
    fmt.Println(payload) // map[iat:1.516239022e+09 name:John Doe sub:1234567890]
}

// One-liner in Go playground:
// base64.RawURLEncoding.DecodeString(strings.Split(token, ".")[1])

7. PHP One-Liner

PHP's base64_decode() is tolerant of missing padding and can handle URL-safe characters after a simple strtr() translation. This works in any PHP version.

<?php
// PHP — decode JWT payload in one line
$payload = json_decode(base64_decode(strtr(explode('.', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')[1], '-_', '+/')), true);
print_r($payload);

// Output:
// Array ( [sub] => 1234567890 [name] => John Doe [iat] => 1516239022 )

// Decode header:
$header = json_decode(base64_decode(strtr(explode('.', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')[0], '-_', '+/')), true);
print_r($header);

// Output:
// Array ( [alg] => HS256 [typ] => JWT )

// As a reusable function:
function jwt_decode_payload(string $token): array {
    return json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true);
}
?>

8. Why This Works: Base64URL vs Base64

JWT uses Base64URL encoding (RFC 4648 Section 5), which differs from standard Base64 in exactly two characters. Understanding this difference is the key to decoding JWTs without a library.

Standard Base64 uses + and / as the 62nd and 63rd characters, with = for padding. Base64URL replaces + with - and / with _, and strips the padding. This makes JWT tokens safe to embed in URLs, HTTP headers, and cookies without escaping.

CharacterBase64 (Standard)Base64URL (JWT)
62nd character+-
63rd character/_
Padding= (required)Stripped (omitted)
URL-safe?No (needs percent-encoding)Yes
// Conversion formula (any language):
base64url_to_base64(s) = s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice(0, (4 - s.length % 4) % 4)
base64_to_base64url(s) = s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')

9. Security Warning: Decode != Verify

Decoding a JWT does NOT verify it. Anyone can create a JWT with any payload. Decoding only reveals the claims inside the token; it tells you nothing about whether the token is authentic, unexpired, or unmodified.

  • Never trust decoded payload data without verifying the signature first.
  • Never use manual decoding for authentication or authorization decisions.
  • The signature (third part) requires the secret key or public key to verify.
  • Use a vetted library (jsonwebtoken, PyJWT, go-jwt) for any security-sensitive operation.
  • Manual decoding is only appropriate for debugging, logging, and inspecting token contents.
// WRONG: Using decoded payload for auth decisions
const user = JSON.parse(atob(token.split('.')[1]));
if (user.role === 'admin') grantAccess(); // DANGEROUS — token could be forged!

// RIGHT: Verify first, then trust the payload
const verified = jwt.verify(token, SECRET_KEY);     // Throws if invalid
if (verified.role === 'admin') grantAccess();        // Safe — signature checked

10. Common Pitfalls

Missing Padding

JWT strips Base64 padding (=) to keep tokens compact. Most Base64 decoders expect padding. Fix: append = characters until the string length is a multiple of 4, or use a decoder that handles unpadded input (like Go's RawURLEncoding or Python's urlsafe_b64decode with manual padding).

URL-Safe Characters Not Replaced

Standard atob() and many Base64 decoders only accept + and /. If you forget to replace - and _ back to + and /, the decoder will throw an error or produce garbage output.

UTF-8 Encoding Issues

If the JWT payload contains non-ASCII characters (e.g., names in Chinese, Arabic, or emoji), atob() will break because it returns Latin-1, not UTF-8. In browsers, wrap with decodeURIComponent(escape(atob(...))) or use TextDecoder. Node.js Buffer handles UTF-8 correctly by default.

Decoding the Signature

The third part is a binary hash, not JSON. Do not try to JSON.parse() the signature. It will fail. The signature is only useful when verified against the header + payload using the signing key.

// Fix padding issue (JavaScript):
const pad = (s) => s + '='.repeat((4 - s.length % 4) % 4);
JSON.parse(atob(pad(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))))

// Fix UTF-8 issue (Browser):
const decodeJWT = (token) => {
  const base64 = token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/');
  const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
};

11. When to Use a Library Instead

Manual decoding is perfect for quick inspection, but you should use a proper JWT library when:

  • You need to verify the signature (authentication and authorization).
  • You need to check expiration (exp), not-before (nbf), and other time-based claims.
  • You are handling RS256/ES256 tokens with public/private key pairs.
  • You need to create (sign) new tokens, not just read existing ones.
  • You are building middleware that gates access to protected routes.
LanguageLibraryInstall
Node.jsjsonwebtokennpm install jsonwebtoken
PythonPyJWTpip install PyJWT
Gogolang-jwt/jwtgo get github.com/golang-jwt/jwt/v5
PHPfirebase/php-jwtcomposer require firebase/php-jwt
JavajjwtMaven: io.jsonwebtoken:jjwt-api
Rustjsonwebtokencargo add jsonwebtoken

Decode JWT tokens instantly with our JWT Decoder tool

FAQ

Can I decode a JWT without any library?

Yes. A JWT payload is just a Base64URL-encoded JSON string. You can decode it with built-in Base64 functions available in every programming language. You only need a library to verify the signature.

Is it safe to decode a JWT on the client side?

Decoding on the client side is safe for display purposes (e.g., showing the user's name or checking the expiration time in the UI). However, never make security decisions based on client-side decoding alone, because the token could be forged. Always verify the signature on the server.

Why does atob() fail on some JWT tokens?

atob() expects standard Base64, but JWT uses Base64URL encoding which replaces + with - and / with _. You must convert these characters back before calling atob(). Additionally, atob() does not handle UTF-8 multi-byte characters correctly; use TextDecoder for non-ASCII payloads.

What is the difference between Base64 and Base64URL?

Base64URL (RFC 4648 Section 5) replaces + with - and / with _ to make the output URL-safe. It also strips padding (=) characters. JWT uses Base64URL so tokens can appear in URLs, cookies, and HTTP headers without encoding issues.

Can I modify a decoded JWT and use it?

You can decode and modify the payload, but the modified token will have an invalid signature. Any server that properly verifies signatures will reject it. This is exactly how JWT prevents tampering: the signature covers the exact header and payload bytes.

𝕏 Twitterin LinkedIn
War das hilfreich?

Bleiben Sie informiert

Wöchentliche Dev-Tipps und neue Tools.

Kein Spam. Jederzeit abbestellbar.

Verwandte Tools ausprobieren

JWTJWT DecoderB64Base64 Encoder/DecoderJPJWT ParserJVJWT Validator

Verwandte Artikel

Wie JWT funktioniert: Vollständiger Guide zu JSON Web Tokens

Verstehen Sie JWT-Authentifizierung, Header, Payload und Signatur.

API-Authentifizierung: OAuth 2.0 vs JWT vs API Key

API-Authentifizierungsmethoden vergleichen: OAuth 2.0, JWT Bearer Tokens und API Keys. Wann welche Methode verwenden.