JWT Secret Key Generator
A cryptographically random HMAC secret, sized correctly for HS256, HS384 or HS512. Generated on your machine and never transmitted.
crypto.getRandomValues(). Generated on your machine; never sent anywhere.What an HMAC secret has to be
When you sign a JWT with HS256, HS384 or HS512, the "key" is just a sequence of random bytes shared between whoever mints tokens and whoever verifies them. There is no structure to it, no public half, no certificate. Which sounds forgiving — and is exactly why it goes wrong so often. A key with no structure has nothing to validate, so "secret" is accepted as readily as 32 bytes of entropy, and every JWT library will happily sign with either.
A usable HMAC secret needs three properties, and the first one is the one people miss:
- Random. Not chosen, not derived from a word, not "the project name plus the year". Generated by a cryptographically secure random number generator.
- Long enough. At least the width of the hash — 256 bits for HS256.
- Secret. Which sounds obvious, and is nonetheless the property most often lost — to a committed
.env, a Slack message, a screenshot in a ticket, or a CI log.
Length: match the hash
RFC 7518 is unambiguous here. A key used with HMAC-SHA256 must have a length equal to or greater than the output of the hash. In practice:
| Algorithm | Hash | Minimum secret |
|---|---|---|
HS256 | SHA-256 | 256 bits — 32 bytes |
HS384 | SHA-384 | 384 bits — 48 bytes |
HS512 | SHA-512 | 512 bits — 64 bytes |
Note that "32 bytes" is a count of bytes, not characters of some encoding. A 256-bit secret is 44 characters in base64, or 64 characters in hex — both are the same 32 bytes underneath. Going longer than the minimum buys you nothing: HMAC folds a key longer than the hash's block size back down by hashing it first, so a 4096-bit secret is not meaningfully stronger than a 512-bit one.
Going shorter, on the other hand, costs you a great deal — and short is exactly what a human-chosen passphrase is.
Why a weak secret is worse than it sounds
Attacking a JWT's HMAC secret is an offline attack, and that is the whole problem. The attacker needs one token — from a log file, a browser's local storage, a leaked HAR file, a support screenshot. That single token contains the signing input and the resulting MAC. They can now guess secrets on their own hardware, as fast as they can compute SHA-256, with no rate limit, no lockout, no alerting, and no way for you to notice.
Commodity tooling does this at billions of guesses per second on a GPU. A dictionary word falls immediately. A word with digits appended falls immediately. Anything a human chose and can remember falls within hours. Against 256 bits of real entropy, the same hardware makes no progress at all, ever.
And the payoff is total: with the secret, the attacker mints valid tokens for any user, with any claims — administrator, another tenant, whatever they like. Every downstream authorization decision that trusts your JWTs is now theirs to make. There is no detection surface, because the tokens are genuinely, cryptographically valid.
Secrets that appear in tutorials — secret, your-256-bit-secret, changeme — are effectively public. Our validator flags them when it sees one.
Generating a secret outside the browser
For anything real, generate the secret where it is going to live. Any of these produce the same 32 bytes of secure randomness this page does:
# OpenSSL — base64, 32 bytes
openssl rand -base64 32
# OpenSSL — hex
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Python
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
# Linux / macOS, no dependencies
head -c 32 /dev/urandom | base64Note what is absent from that list: Math.random(), uuid4(), the current timestamp, and anything from a language's default (non-cryptographic) random module. Those are designed to be fast and evenly distributed, not unpredictable — their output can often be reconstructed from a few prior values. Use the crypto-specific API in whatever language you are in, every time.
Storing it
The secret belongs in an environment variable injected at deploy time, or better, in a dedicated secret store — AWS Secrets Manager, GCP Secret Manager, Vault, or your platform's equivalent. What it must never be is a string literal in source code, and the reason is not merely that someone might read the repo: once a secret is committed, it lives in git history forever, gets copied into every fork and clone and CI cache, and outlives every attempt to delete the line.
Then, in rough order of how often each is skipped:
- One secret per environment. Staging must not be able to mint tokens that production will accept. If they share a key, staging is production's blast radius.
- Never log it. Not at debug level, not in an error path, not in an exception's context object. Log lines get shipped to third-party aggregators.
- Keep it out of the client. An HMAC secret in a browser bundle or a mobile app binary is not a secret. If a public client needs to verify tokens, you need an asymmetric algorithm instead — see the generator for RS256, ES256 and EdDSA.
Rotating it
Plan for rotation before you need it, because you will need it in a hurry: a departing employee, a leaked log, a dependency compromise. A rotation you have never rehearsed during an incident is not a plan.
The mechanics are straightforward:
- Add the new secret alongside the old one, and configure verification to accept either.
- Switch signing to the new secret only.
- Wait out the longest token lifetime you issue — every token signed with the old secret must have expired.
- Remove the old secret from the verifier.
This is materially easier if your tokens carry a kid header naming which key signed them, so the verifier can select the right one directly instead of trying each in turn. It also makes step 3 auditable — you can see, in your logs, when the last token bearing the old kid was presented.
Note that step 3 is the reason short token lifetimes are worth the trouble. If your access tokens live for 15 minutes, a full rotation completes in 15 minutes. If they live for 30 days, a compromised key stays usable for 30 days — you cannot revoke a JWT, you can only outlast it.
Common questions
How long should a JWT secret be?
At least as long as the hash it feeds. HS256 uses SHA-256, so the secret should be at least 256 bits (32 bytes); HS384 wants 384 bits and HS512 wants 512. RFC 7518 makes this a requirement, not a suggestion. A shorter key weakens the MAC, and a short human-chosen passphrase is worse still — it is brute-forceable offline by anyone holding one token.
Is it safe to generate a production secret in a browser?
The randomness is fine — crypto.getRandomValues() is a cryptographically secure source, the same one your browser uses for TLS. The exposure is the question. A secret that appears on a screen can be shoulder-surfed, screenshotted, synced by a clipboard manager, or captured by a malicious extension. This page never transmits it, but the safest secret is one generated where it will live: in your secret manager, or on the server via `openssl rand -base64 32`.
Should the secret be hex, base64 or plain text?
It doesn't matter cryptographically — what matters is that both sides agree, because they are different byte sequences. This is the single most common cause of a mysterious 'invalid signature'. The literal text 'c2VjcmV0' and the bytes you get by base64-decoding it are two entirely different keys. Pick one encoding, write it down, and be consistent.
Can I use my app's SECRET_KEY or an API key as the JWT secret?
You can, and you should not. Reusing one secret across purposes means a leak anywhere is a leak everywhere, and it makes rotation impossible without breaking unrelated systems. Give the JWT signing key its own identity, its own storage entry, and its own rotation schedule.
How do I rotate a JWT secret without logging everyone out?
Verify against both the old and new secret for a window at least as long as your longest token lifetime, while signing only with the new one. Once every token issued under the old secret has expired, remove it. This is far easier with a kid header, which lets a verifier pick the right key by name instead of trying each in turn.
The rest of the workbench
JWT Decoder
Read a token's header, payload and claims — including expired ones. Nothing is uploaded.
JWT Validator
Verify a signature against a secret, PEM, JWK or JWKS. Signature and expiry are judged separately.
JWT Generator
Mint a test token for any algorithm — including deliberately invalid ones, to test your rejection path.