server
jwt-hack server exposes decode, encode, verify, crack, payload and scan over a local JSON REST API, so you can drive jwt-hack from scripts, a CI step or a web frontend instead of the CLI.
# Default bind is 127.0.0.1:3000
jwt-hack server
# Listen on all interfaces
jwt-hack server --host 0.0.0.0 --port 8080
# Require an API key on every request
jwt-hack server --api-key "$KEY"
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Bind address. Use 0.0.0.0 to accept remote connections |
--port |
3000 |
TCP port |
--api-key |
none | When set, every request must send a matching X-API-KEY header |
Every endpoint takes and returns application/json. CORS is open: any origin, method and header. If the port is already in use the server prints an error and exits non-zero rather than crashing.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET, POST | /health |
Liveness check with the running version |
| POST | /decode |
Decode a token into header, payload and algorithm |
| POST | /encode |
Sign a JWT from JSON claims |
| POST | /verify |
Check an HMAC signature |
| POST | /crack |
Recover a weak HMAC secret by dictionary or brute force |
| POST | /payload |
Generate attack tokens |
| POST | /scan |
Run basic checks and an optional weak-secret probe |
An unknown route returns 404 and malformed JSON returns a 4xx. Application-level failures (bad token, wrong mode) come back as HTTP 200 with "success": false and an error string, so a client should check the success field, not just the status code. With --api-key set, a missing or wrong key returns 401.
Health
curl -s http://127.0.0.1:3000/health
{"status":"ok","version":"2.6.0"}
Decode
Request {"token": "..."}. The algorithm field reports the real alg from the header.
curl -s http://127.0.0.1:3000/decode \
-H 'Content-Type: application/json' \
-d '{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}'
{"success":true,"header":{"alg":"HS256","typ":"JWT"},"payload":{"sub":"1234567890","name":"John Doe","iat":1516239022},"algorithm":"HS256","error":null}
Encode
| Field | Default | Description |
|---|---|---|
payload |
required | JSON claims object |
secret |
"" |
HMAC secret (HS256/384/512) |
algorithm |
HS256 |
Signing algorithm |
no_signature |
false |
When true, emit an unsigned alg:none token and ignore secret/algorithm |
headers |
none | Extra header params as an array of [key, value] pairs |
compress |
false |
DEFLATE the payload and add "zip":"DEF" |
Encoding over the API uses HMAC secrets or none. Signing with asymmetric private keys is not exposed here; use the CLI encode for that.
curl -s http://127.0.0.1:3000/encode \
-H 'Content-Type: application/json' \
-d '{"payload":{"sub":"1234567890","name":"John Doe"},"secret":"secret","algorithm":"HS256","headers":[["kid","abc123"]]}'
{"success":true,"token":"eyJ...","error":null}
Verify
Request {"token": "...", "secret": "...", "validate_exp": false}. Verification is HMAC only; an RSA/ECDSA/EdDSA token will not validate here.
curl -s http://127.0.0.1:3000/verify \
-H 'Content-Type: application/json' \
-d '{"token":"<token>","secret":"your-256-bit-secret"}'
{"success":true,"valid":true,"error":null}
If the token's header is alg:none and you send a non-empty secret, the server reports valid:false with an explanation instead of silently accepting it, so the none bypass is not mistaken for a valid signature.
Crack
| Field | Default | Description |
|---|---|---|
token |
required | Token to attack |
mode |
dict |
dict or brute |
wordlist_content |
none | Inline array of candidate secrets (preferred for dict) |
wordlist |
none | Server-side file path. Disabled unless gated by JWT_HACK_WORDLIST_DIR (see below) |
preset |
none | Brute charset preset: az, AZ, aZ, 19, aZ19, ascii |
chars |
a-z0-9 |
Brute charset when no preset is given |
min / max |
1 / 4 |
Brute length range |
concurrency |
20 |
Accepted for compatibility, not used by the current code |
# Dictionary with an inline wordlist
curl -s http://127.0.0.1:3000/crack \
-H 'Content-Type: application/json' \
-d '{"token":"<token>","mode":"dict","wordlist_content":["secret","password","hunter2"]}'
# Brute force
curl -s http://127.0.0.1:3000/crack \
-H 'Content-Type: application/json' \
-d '{"token":"<token>","mode":"brute","preset":"aZ19","max":3}'
{"success":true,"secret":"secret","error":null}
When nothing matches you get {"success":true,"secret":null,"error":"Secret not found"}.
Cracking runs on a blocking thread so it can't stall /health and the other endpoints, and at most four crack or scan jobs run at once; extra requests wait for a slot. Brute force rejects a keyspace larger than 5,000,000 candidates and a max beyond the built-in brute-force length limit, so a single request can't pin a worker indefinitely.
Server-side wordlist paths
The wordlist file path is a remote-controlled filesystem read, so it is off by default. To enable it, set JWT_HACK_WORDLIST_DIR to a directory; only paths that canonicalize to inside that directory are honored, which blocks .. traversal and symlink escapes. Files must be regular files and are read up to 64 MiB. Prefer wordlist_content so clients never depend on server-side files.
Payload
| Field | Default | Description |
|---|---|---|
token |
required | Source token |
target |
all | Payload type, same values as the CLI payload command |
jwk_trust |
none | Trusted domain for jku/x5u scenarios |
jwk_attack |
none | Attacker domain for jku/x5u scenarios |
jwk_protocol |
https |
http or https |
public_key |
none | Server public key (PEM) for RS256-to-HS256 confusion forging |
curl -s http://127.0.0.1:3000/payload \
-H 'Content-Type: application/json' \
-d '{"token":"<token>","jwk_attack":"attacker.tld","target":"all"}'
{"success":true,"payloads":[{"name":"Payload #1","token":"eyJ..."},{"name":"Payload #2","token":"eyJ..."}],"error":null}
See payload for what each target produces.
Scan
| Field | Default | Description |
|---|---|---|
token |
required | Token to inspect |
skip_crack |
false |
Skip the weak-secret probe |
wordlist_content |
none | Inline wordlist for the weak-secret probe |
wordlist |
none | Server-side path, same JWT_HACK_WORDLIST_DIR gating as /crack |
skip_payloads |
false |
Accepted but unused; scan does not generate payloads |
max_crack_attempts |
100 |
Accepted but not enforced |
curl -s http://127.0.0.1:3000/scan \
-H 'Content-Type: application/json' \
-d '{"token":"<token>","wordlist_content":["secret","your-256-bit-secret"]}'
{"success":true,"vulnerabilities":["Weak secret found: your-256-bit-secret"],"secret":"your-256-bit-secret","error":null}
The checks are: alg:none, an expired exp, an unparseable token (Invalid token format), and, unless skip_crack is set and a wordlist is supplied, a weak-secret probe that reports the secret both in the vulnerabilities list and the top-level secret field.
Exposure
This is a testing tool, not a hardened service. If you bind it beyond localhost, set --api-key to require X-API-KEY on every request (the key is compared in constant time), and put it behind your own auth, rate limiting and isolation. The /crack and /scan endpoints are CPU-heavy by design.