JWT Brute Force Is an Offline Attack
JWT brute force attacks are not theoretical. Researchers have published databases of common JWT secrets found in production applications. If your secret is short, human-readable, or copied from a tutorial, it may already be in those databases — and cracking it does not require hitting your API at all.
This post explains how offline HMAC cracking works, how fast modern hardware tries candidates, which secret shapes fail immediately, and how to generate secrets that remain out of reach. Use the JWT Secret Generator when you need a browser CSPRNG, then store the result outside source control.
How JWT Brute Force Works
A JWT consists of three base64url-encoded segments: header.payload.signature. For HS256, the signature is an HMAC — a keyed hash of header + "." + payload using your secret.
An attacker who intercepts a valid JWT can attempt to crack the secret offline:
1. Extract the header and payload (base64url-decode them for inspection; keep the raw signing input for HMAC).
2. Try candidate secrets from a wordlist or ruleset.
3. For each candidate, compute HMAC-SHA256(candidate, header + "." + payload).
4. If the result matches the token's signature, the candidate is the real secret.
This requires no server access, no rate limiting, and no detection on your side. Compute happens on the attacker's hardware. Once the secret falls, every token can be forged until you rotate.
Validate unfamiliar tokens with the JWT Validator during investigations — but remember validation with the real secret is not the same as resisting offline search.
Attack Speed on Modern Hardware
| Hardware | HS256 Attempts/Second |
|---|---|
| Consumer CPU (single core) | ~50,000 |
| Consumer CPU (multi-core) | ~500,000 |
| Consumer GPU (RTX 4090) | ~200,000,000 |
| Cloud GPU cluster (8x A100) | ~2,000,000,000 |
Dictionary and rule-based attacks are far faster than pure brute force against short human strings. Hashcat and jwtcrack-style tooling make this accessible to hobbyists, not only nation-states.
Time to Crack by Secret Type
| Secret | Entropy | Time to Crack (GPU cluster) |
|---|---|---|
| `"secret"` | ~40 bits | Milliseconds (dictionary) |
| `"myapp_jwt_2024"` | ~50 bits | Minutes (rule-based attack) |
| 8-char alphanumeric | ~47 bits | Hours |
| 16-char alphanumeric | ~95 bits | Years |
| 256-bit CSPRNG hex | 256 bits | Longer than the age of the universe |
The formula for exhaustive search is T = S / H where S is the search space (2^entropy_bits) and H is the hash rate. At roughly 2 × 10^9 attempts per second against a true 256-bit random secret: T = 2^256 / 2×10^9 ≈ 5.8 × 10^67 seconds.
Visual length is not entropy. A 64-character passphrase of English words can be weaker than a 32-byte random key. Prefer random bytes from a CSPRNG, then encode as hex or base64 for transport. See HS256 secret length and the HS256 glossary.
Real-World Secrets Found in Production
Security researchers using tools like jwtcrack and hashcat have found these secrets in production JWTs:
"secret"— the default in most tutorials"your-256-bit-secret"— literally copied from documentation examples"password","admin","test"- Company names, product names, developer GitHub usernames
"jwt_secret","mysecretkey","supersecretkey"
These appear because developers copy example code and forget to replace placeholder secrets before deploying. Treat any tutorial default as already public.
How to Generate a Safe Secret
Use a CSPRNG — a cryptographically secure pseudorandom number generator.
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex'); // 256 bits
console.log(secret);import secrets
print(secrets.token_hex(32))package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func main() {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err)
}
fmt.Println(hex.EncodeToString(b))
}In the browser, the JWT Secret Generator uses crypto.getRandomValues(), the platform CSPRNG.
Critical properties of a CSPRNG-generated secret:
- Statistically uniform distribution — no patterns
- Unpredictable — knowledge of previous outputs does not help predict future outputs
- No relationship to time, process ID, or any observable state
Defence Checklist
- [ ] Use a minimum 256-bit CSPRNG secret for HS256
- [ ] Never use human-readable strings as secrets
- [ ] Never reuse secrets from tutorials, examples, or documentation
- [ ] Use different secrets for dev, staging, and production
- [ ] Store in environment variables or a secrets manager — never in source code
- ] Rotate annually or after any suspected exposure ([rotation guide)
- [ ] Pin algorithms on verify (
HS256allowlist)
Frequently Asked Questions
Can rate limiting stop JWT secret brute force?
Not for offline attacks. Rate limiting protects online login endpoints. Once an attacker has a valid JWT, HMAC trials happen locally against the signature with no requests to your servers.
Is a long passphrase safer than a short random hex string?
Not necessarily. Passphrases can look long while packing little entropy. A 32-byte CSPRNG value (64 hex characters) is the practical HS256 baseline regardless of how "readable" a passphrase feels.
Does RS256 eliminate brute-force risk on the signing key?
Asymmetric signing moves the threat model: verifiers hold a public key and cannot forge with it. Private keys still must be strong and protected. For trade-offs, read HS256 vs RS256.
What should I do if I used a tutorial secret in production?
Generate a new CSPRNG secret immediately, deploy a grace-period rotation, invalidate refresh sessions if compromise is likely, and scrub git history if the old secret was ever committed.