Sometimes you need a token, not a token library: a fixture for an integration test, a bearer token for a curl against your staging API, or a known-good reference to compare against the tokens your own code mints. This generator signs real JWTs with HS256, HS384, HS512, RS256 and ES256, entirely in your browser, and its verify panel recomputes the signature of any token you paste, which is the half that actually solves "invalid signature" bugs.
What this generator does
You type the payload as JSON, the tool builds the header for you, signs both with the key from step 02, and renders the token with its three segments in three colours: header green, payload amber, signature muted. Everything runs live, so editing a claim or flipping the algorithm re-signs immediately.
The signing itself is the Web Crypto API (crypto.subtle.importKey and crypto.subtle.sign), the same primitives your backend runtime uses. Nothing is uploaded and nothing is stored: the algorithm choice survives your next visit, the secret and keys deliberately do not. That is also why it is fine to type a test secret here at all, and it is the reason this page works offline once loaded.
A few things this tool does on purpose that most JWT generators skip: it generates RSA and EC key pairs in the browser and hands you both PEM halves, it reads your HS secret as UTF-8, hex or Base64 because secrets are issued in all three encodings, it warns when an HS256 secret is shorter than the 32 bytes RFC 7518 demands, and its verify panel recognises the DER-instead-of-raw ES256 signature bug by shape.
Building a token, step by step
Step 01 is the payload, plain JSON. Any claims you like go in; the seven registered ones (iss, sub, aud, exp, nbf, iat, jti) have agreed meanings, everything else is between you and your verifier. If you want to read a foreign token's claims rather than write your own, that is the JWT decoder, the reverse of this page.
The time claims are the fiddly part, because they are Unix timestamps in seconds and nobody computes "now plus two hours" in their head. The claims bar does it: type a duration like +1h, 30m, 2d or 1h30m and hit set exp or set nbf; the claim lands in the payload as a proper epoch number. set iat stamps the current time, and the --auto-iat flag re-stamps iat on every signing, which keeps fixtures fresh while you edit.
The header is not editable as text because there is almost nothing to edit: alg follows the algorithm buttons and typ is JWT. The one header field people legitimately need, the key id, has its own kid input in the toolbar; fill it and the header becomes {"alg":"HS256","typ":"JWT","kid":"…"}. The effective header is always displayed above the token, so nothing is signed that you have not seen.
HS256, RS256 or ES256
| Algorithm | Type | Key | Signature size | Typical use |
|---|---|---|---|---|
HS256 | HMAC-SHA-256, symmetric | shared secret, ≥ 32 bytes | 32 bytes | sessions and APIs verified by the issuer itself |
HS384 / HS512 | HMAC, symmetric | ≥ 48 / 64 bytes | 48 / 64 bytes | same, with a longer hash |
RS256 | RSASSA-PKCS1-v1_5, asymmetric | RSA 2048+ private key | 256 bytes at 2048 bit | OIDC id tokens, multi-service verification |
ES256 | ECDSA P-256, asymmetric | EC private key | 64 bytes | same trust model as RS256, quarter of the token weight |
The choice is really about who verifies. With HMAC, verifying and signing are the same operation with the same secret, so every service that checks tokens could also mint them. That is fine when issuer and verifier are one codebase. It stops being fine the moment a second team, a mobile app backend or a partner API needs to verify: now the secret is in three places and a leak anywhere forges tokens everywhere. RS256 and ES256 fix that by splitting the key, and the public half can sit on a JWKS endpoint where verifiers fetch it themselves.
Between the two asymmetric options, ES256 produces a 64-byte signature against RS256's 256 bytes, which matters when the token rides along in every request header. RS256 remains the default of most identity providers for compatibility reasons. Match whatever your verifier ecosystem already speaks, and only pick freely when you control both ends, in which case ES256 is the better default.
Secrets, key pairs and lengths
For the HS algorithms, the secret field reads your input in one of three encodings, and picking the wrong one is the most common cause of a signature that "should" match but does not. utf-8 treats the characters as bytes, which fits passphrase-style secrets. base64 first decodes the string, which is what some identity providers mean when they show a "base64 encoded" client secret. hex reads two digits per byte. Same string, three different byte sequences, three different signatures.
Length matters more than people expect. RFC 7518 requires the HMAC key to be at least as long as the hash output, so 32 bytes for HS256, and the tool warns below that. The threat is concrete: anyone holding one signed token can brute-force candidate secrets offline against its signature, and hashcat mode 16500 exists precisely for JWTs. A random 32-byte secret is out of reach; a dictionary word is minutes of work on a laptop.
For RS256 and ES256 the key panel accepts a PKCS#8 private key, the format that begins with -----BEGIN PRIVATE KEY-----. The Web Crypto API imports nothing else, so the traditional OpenSSL formats (BEGIN RSA PRIVATE KEY, BEGIN EC PRIVATE KEY) get a message with the one-line conversion instead of a generic failure: openssl pkcs8 -topk8 -nocrypt -in key.pem. The generate button builds a fresh 2048-bit RSA or P-256 pair in the tab and shows the public key next to it, ready to paste into the verifier you are testing. Generated keys never leave the page; reload and they are gone, which is exactly right for keys made in a browser.
Debugging an invalid signature
The verify panel is the part we built this page for. Paste a token out of your app, put the secret or key it should have been signed with in step 02, and the tool recomputes the signature and compares byte for byte. That answers the question a decoder cannot: the decoder deliberately never asks for your secret and only shows what the token claims about itself, while this page checks whether the signature is actually right.
The verdicts are written for the real failure modes, roughly in the order we meet them:
- Wrong secret encoding. The recomputed signature differs and the message says so, naming the encoding switch. Providers that issue Base64 secrets bite everyone once.
- Algorithm mismatch. The token says
alg=HS512, the tool is set to HS256; you get a warning naming both before any byte comparison, because comparing across algorithms proves nothing. - DER instead of raw r||s. An ES256 signature of 70-odd bytes starting with
0x30is ASN.1 DER, the default output of OpenSSL and Node's signer. JWS wants the two 32-byte integers concatenated. The tool flags it, converts, and still tells you whether the underlying bytes match, so you learn both that the format is wrong and that the key was right. - alg=none. An unsigned token is named as such rather than "verified", with a pointer to why accepting it would be a vulnerability.
- Expired or not-yet-valid. A correct signature on an expired token is still a rejected token; the panel notes
expandnbfso you do not chase a signature bug that is actually a clock.
If the token in question is a webhook signature rather than a JWT (a Stripe-Signature or X-Hub-Signature-256 header), the computation is a raw-body HMAC, and the HMAC generator is the page built for that.
What the three segments contain
A signed JWT is base64url(header).base64url(payload).base64url(signature). The first two segments are just JSON with an encoding that survives URLs and headers; the alphabet swaps +/ for -_ and drops the = padding entirely, which is why a JWT segment fails in a strict standard-Base64 decoder. The Base64URL converter handles exactly this alphabet if you want to take a segment apart by hand.
The signature is computed over the literal string header.payload, the encoded segments joined by the dot, not over the JSON. That detail matters when you reimplement: re-serialising the JSON with different key order or whitespace produces a different signing input and a different signature, even though the claims are identical. It is also why the token this page shows you cannot be reformatted after signing.
None of the three segments is encrypted. Anyone holding the token reads the payload with twelve characters of JavaScript, and the signature only proves who signed it, not who may read it. What follows from that for storing tokens and for what belongs in a payload is its own topic, covered in where to store JWTs.
Test tokens vs production tokens
Tokens signed here are for development: fixtures, local API testing, reproducing a decoding bug, checking what your middleware does with an expired exp. Production tokens should be minted by your backend with a secret that has never existed outside it. That is not a limitation of this page; it is what secrets mean. A signing key defines who can create identity, so the set of machines that ever saw it should be as small as your infrastructure allows, and no online tool, this one included, needs to be in that set.
The practical workflow that follows: generate a throwaway secret or key pair right here, wire it into the service under test, and throw both away when you are done. For HS256 that is one field; for RS256 and ES256 the generate button gives you the private key for the signer and the public key for the verifier in one click. When the test is over, nothing needs cleaning up, because nothing was stored anywhere to begin with.
Signing questions
Should I use HS256 or RS256 to sign JWTs?
HS256 when one party both issues and verifies the token, RS256 or ES256 when different parties do. HS256 is a symmetric HMAC: every service that can verify a token can also mint one, so handing the secret to a second team or a third-party API turns every verifier into an issuer. RS256 splits the roles: the private key signs, the public key verifies, and you can publish the public key (usually as a JWKS endpoint) without giving anyone the ability to forge tokens. Inside a single backend, HS256 is simpler and faster; the moment tokens cross a trust boundary, use an asymmetric algorithm.
How do I generate an RSA key pair for RS256?
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem writes a PKCS#8 private key, and openssl pkey -in private.pem -pubout -out public.pem derives the public half. For ES256 the pair is openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out private.pem with the same -pubout step. The generator on this page does the equivalent in the browser via crypto.subtle.generateKey and exports both halves as PEM, which is enough for development; keys that protect real users should be generated and stored inside your infrastructure.
What do exp, iat and nbf mean in a JWT?
All three are Unix timestamps in seconds. exp is the expiry: verifiers reject the token once the current time passes it. iat is issued-at, the moment the token was created; it does not expire anything by itself but lets a server judge token age or revoke everything issued before a date. nbf is not-before: the token is invalid until that time arrives, useful for tokens that should activate later. A verifier compares all three against its own clock, which is why a few seconds of clock skew between servers shows up as "token used before issued" errors; most libraries accept a leeway of 30 to 60 seconds for that reason.
How long should the secret for HS256 be?
At least as long as the hash output: 32 bytes for HS256, 48 for HS384, 64 for HS512. RFC 7518 makes that a MUST, and shorter secrets cut the effective security below what the algorithm promises. A password-style secret like "s3cret42" is brute-forceable offline by anyone who captures a single token, because the attacker can test candidate secrets against the signature at billions of guesses per second. Generate the secret randomly (openssl rand -base64 32) instead of choosing something memorable; nothing ever types it by hand.
What is the kid header parameter in a JWT?
A key id: an opaque string that tells the verifier which key signed this token, so it can pick the right one out of a set. It matters during key rotation, where tokens signed with the old and the new key circulate at the same time, and with JWKS endpoints, where the verifier matches kid against the keys the issuer publishes. The value carries no meaning of its own; "2024-07-key1" and a UUID work equally well. What matters is that the verifier looks keys up by kid rather than trusting any key material embedded in the token itself.
Why do JWTs use base64url instead of normal Base64?
Because tokens travel in URLs and HTTP headers, where + / and = are unsafe. Base64url swaps + for - and / for _, and JWS additionally forbids the = padding, so token length alone tells the decoder how many bytes to expect. This is why pasting a JWT segment into a plain Base64 decoder sometimes fails: a - or _ in the segment is not in the standard alphabet. It also means a signature containing + or / is a bug in the issuing code, typically a library defaulting to standard Base64.
What is the alg=none attack on JWT?
The JWT spec defines "none" as a valid algorithm meaning the token is unsigned, with an empty third segment. Early libraries read the alg field from the attacker-controlled header and honoured it, so an attacker could strip the signature, set alg to none, and have the token accepted as valid. The fix, standard in every maintained library since around 2015, is that the verifier states which algorithms it accepts and never lets the token choose. If you write verification by hand, hard-code the expected algorithm; comparing header.alg against a list you control is the whole defence.
Why is my ES256 JWT signature invalid even though the key is right?
Usually because the signature is ASN.1 DER instead of the raw 64-byte r||s concatenation JWS requires. OpenSSL and Node's crypto.sign produce DER by default for ECDSA, so a hand-rolled ES256 issuer emits signatures of 70 to 72 bytes that spec-compliant verifiers reject. In Node, pass dsaEncoding: "ieee-p1363" to crypto.sign; elsewhere, convert by parsing the DER SEQUENCE and left-padding r and s to 32 bytes each. The verify panel on this page detects a DER-shaped ES256 signature and says so, which turns a silent mismatch into a one-line diagnosis.
How do I create a JWT in Node.js without a library?
Three lines of node:crypto. Build the two segments with Buffer.from(JSON.stringify(obj)).toString("base64url"), join them with a dot, then sign: crypto.createHmac("sha256", secret).update(input).digest("base64url") for HS256, or crypto.sign("sha256", Buffer.from(input), { key, dsaEncoding: "ieee-p1363" }) for ES256. Concatenate input, a dot and the signature. Verification is the same computation plus crypto.timingSafeEqual for the comparison. For anything beyond a script, a maintained library such as jose is still worth it, because it also enforces algorithm allow-lists and claim checks you would otherwise forget.
Is it safe to paste a signing secret into an online JWT generator?
Treat any secret pasted into any website as potentially logged, because with most online tools the request that computes the token carries your secret to their server. The exception is tools that sign locally: this page keys the Web Crypto API inside your browser tab, the secret is excluded from the saved options, and no request with your input ever leaves the tab, which you can confirm in the network panel of your devtools. Even then, the sane rule stays: use throwaway secrets for experiments, and never paste the secret that protects production sessions into anything outside your infrastructure.