JWT Generator / Signer
Build a header and payload, sign with your own key, and copy the token — the key never leaves this tab.
This token is signed with your key — mint test credentials only.
The selected algorithm is stamped into the header as alg — this tool never reads it off the key.
Extra protected-header members go here. alg comes from the selector; typ defaults to JWT.
The claims set. Registered claims below are written into this draft.
Helpers win over the same claim typed in the payload editor, and say so.
The key stays in this tab: it is never saved, never put in a share link, and never sent anywhere.
Paste a signing key to enable Sign.
The exact bytes the algorithm signed — compare this against what your verifier reports receiving.
The link carries the header and payload only — never the key and never the token.
Engine messages from the last run appear here.
Declared scope
- alg:none is refused: a verifier that honours "alg":"none" accepts a token anyone can forge, so this tool never emits one.
- ES256K is refused — Web Crypto does not implement secp256k1.
- Ed448 is refused — Web Crypto does not implement it.
- X448 is refused — it is a key-agreement curve, not a signing algorithm.
Press ? for shortcuts
Do it offline:$
How to use it
- Choose the algorithm first. That choice decides which key shapes are accepted, how long the signature will be, and what gets stamped into the header's alg member. HS256, HS384 and HS512 take a shared secret; the RS, PS and ES families and EdDSA take a private key.
- Author the claims set in the payload pane, as ordinary JSON: registered names such as iss, sub and aud, and any private claim your service reads. The pane re-serialises compactly before encoding, so formatting is yours to keep readable.
- Reach for the claim helpers rather than typing timestamps. The exp preset offers 5 minutes, 1 hour, 24 hours or a custom span; iat stamps the clock the page is holding; nbf takes an offset; jti draws sixteen random bytes. A helper beats a value typed into the pane, and says so when it overrides one.
- Supply the key in whichever shape you have: a JWK, a JWKS with the kid you want selected, a PKCS#8 PEM, or a raw HMAC secret with its encoding named — base64, base64url or UTF-8. Key text is masked until you reveal it.
- Sign, then read the three sections of the token card and copy or download the result. Nothing survives the tab: reloading starts empty, and a share link carries only the header and payload you authored.
Write a header and a claims set, pick one of thirteen signing algorithms, paste the key that should sign it, and read the finished compact token — header, payload and signature, each labelled. Claim helpers fill exp, iat, nbf and jti from a clock you can see. The key is used in this tab and nowhere else: it is never stored, never put in a share link, and never sent anywhere.
How it works
Signing runs in a fixed order. The protected header is serialised to compact JSON and encoded; the claims set is serialised and encoded; the two encoded strings are joined with a single dot. That joined string — the signing input — is what the algorithm actually signs, and the signature is encoded and appended after a second dot (RFC 7515 §5.1).
What gets signed is the encoded text, not the JSON you typed. Reformat the payload pane — change indentation, reorder two members — and the encoded segment changes, so the signature changes with it. That is why a verifier recomputes over the segments exactly as they arrived, and why this page shows the signing input: it is the only thing the algorithm ever sees.
The header's alg member is stamped from the algorithm selector, never read off the key and never taken on trust from the header pane. If the pane carries a different alg, the selector wins and the override is stated in the notes. The same rule refuses the value none outright: a token whose header claims no signature is one anyone can mint.
The time claims are NumericDates — a count of seconds since 1970-01-01T00:00:00Z (RFC 7519 §2). exp says when the token stops being acceptable, nbf when it starts, iat when it was issued. A verifier compares all three against its own clock, usually with a small leeway for skew, so the presets here are written from the clock value the page displays.
Two families behave differently every time you press Sign. ECDSA signatures (ES256, ES384, ES512) carry a fresh random component, and RSA-PSS signatures (PS256, PS384, PS512) carry a random salt whose length equals the hash output — 32, 48 and 64 bytes (RFC 7518 §3.5). Sign twice and the third segment differs; both verify. HMAC, RSASSA-PKCS1-v1_5 and Ed25519 repeat byte for byte, which is what makes them testable against published vectors.
"The key stays here" is structural, not a promise. The key text has no place in the shareable state — the fields do not exist in it — so a link cannot carry a key even if you ask it to, and neither can the downloaded state file. Nothing is written to storage, so there is nothing to clear. The signature is computed by the browser's own WebCrypto implementation; no signing library is shipped.
Examples
{
"iss": "https://issuer.example",
"sub": "user-1234567890",
"aud": "https://api.example",
"iat": 1700000000,
"nbf": 1700000000,
"exp": 1700003600,
"jti": "b9f0a1c2-3d4e-4f50-8a6b-7c8d9e0f1a2b"
}eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvbGRlbi1IUzI1NiJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwic3ViIjoidXNlci0xMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZSIsImlhdCI6MTcwMDAwMDAwMCwibmJmIjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDAsImp0aSI6ImI5ZjBhMWMyLTNkNGUtNGY1MC04YTZiLTdjOGQ5ZTBmMWEyYiJ9.bv6d0AqDRECOxJdk7jEDJJxtiKLMrVHFe5PZUky0TP8A complete claims set signed with a 32-byte shared secret, kid stamped so a verifier holding several keys knows which to try. This is the reference vector the page is tested against: an independent library produced the same 349 characters, compared byte for byte on every run.
{
"iss": "https://auth.example",
"sub": "svc-billing"
}eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZSIsInN1YiI6InN2Yy1iaWxsaW5nIiwiZXhwIjoxNzAwMDAzNjAwLCJpYXQiOjE3MDAwMDAwMDAsImp0aSI6IlptOXZZbUZ5TVRJek5EVTJOemc1TUEifQ.Gm3S5Er9IiYBlRKovSHTzMCLWxcpGbg6XXtJpDhOLIQTwo authored claims plus three helpers. The header pane was an empty object, so typ defaulted to JWT and the note said so; the one-hour preset wrote exp as 1700003600, exactly one hour past the 1700000000 the page was holding; iat took that same second. Helper-written claims land after the authored ones.
{
"sub": "device-77",
"aud": "https://api.example"
}eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvbGRlbi1FUzI1NiJ9.eyJzdWIiOiJkZXZpY2UtNzciLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlIn0Printed as the signing input alone — header, dot, payload — because the third segment cannot be printed honestly. ES256 draws a fresh random value per signature, so signing these claims twice yields two different 64-byte signatures and both verify. The test signs twice, confirms each is 64 raw bytes rather than a DER wrapper, and verifies both.
{
"iss": "https://mesh.example",
"sub": "node-3"
}eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL21lc2guZXhhbXBsZSIsInN1YiI6Im5vZGUtMyJ9.UNH818Js6fpGuPAToeOIsXQsGdOiCm-GbxWNkbDcSdwdNOVE338kJzbFcghItd_9rIYFD_v8jAxLj2xczGjDAAEdDSA over Ed25519 (RFC 8037 §3.1): the same 64 bytes every time, because Ed25519 derives its nonce from the key and the message instead of drawing one. The test reproduces this third segment with a second, unrelated implementation. Browsers gained Ed25519 only recently, so the page detects support and refuses in words when it is missing.
{
"sub": "anyone"
}Algorithm "none" at the algorithm selector — this tool never emits an unsecured JWS, because a verifier that honours "alg":"none" accepts a token anyone can forge (RFC 7515 §4.1.1, the classic JWT vulnerability); choose one of the 13 signing algorithms.The edge case, and the one refusal this page will not negotiate: nothing is assembled and nothing is signed. Unsecured JWSs have a narrow legitimate use, but a tool that mints them on demand mostly supplies material for testing whether someone else's verifier can be talked into accepting one.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
The encoding selector on a shared secret decides the bytes
A secret written as UGSR_PlaZ8l-ATI6K47veMG4Zr40GcYB7f3k3IJY7EE is 43 characters of text and 32 bytes of base64url — two different keys, producing two different signatures. Issuers that publish a secret in base64 sign with the decoded bytes; a memorable phrase is signed as UTF-8. Pick the wrong one and everything succeeds while nothing verifies, so the page names the ambiguity when it sees it.
A secret shorter than the hash output is flagged, not blocked
RFC 7518 §3.2 sets the floor for the HS family at the size of the hash: 32 bytes for HS256, 48 for HS384, 64 for HS512. A shorter secret still signs and still verifies — which is the problem, because it is also the token an offline search can recover the secret from. The warning names the byte count it measured.
A JWKS needs a kid; a JWK does not
Paste a single JWK and there is nothing to choose. Paste a key set and the kid field decides which member signs, and that kid is stamped into the header so the verifier can make the same choice in reverse. If no member carries it, the refusal lists the identifiers the set does contain rather than falling back to the first key.
Private keys arrive as PKCS#8, and openssl does the conversion
The PEM shape this page reads begins BEGIN PRIVATE KEY. The older BEGIN RSA PRIVATE KEY is PKCS#1, still the default output of several tools: openssl pkcs8 -topk8 -nocrypt -in old.pem -out new.pem converts it on your own machine. Pasting a certificate where a private key belongs gets its own named answer.
aud may be one string or a list, and crit is a trap
The audience claim is defined both ways, and whichever form you write is emitted unchanged. The header's crit member is different: it names extensions a verifier must understand or reject, and since nothing here implements one, a crit entry produces a token every conforming verifier must refuse. The page warns per entry rather than dropping the member behind your back.
Size caps refuse rather than truncate
The header pane stops at 8,192 characters and the claims pane at 65,536, and neither is trimmed to fit: a shortened claims set would sign cleanly into a token that says something you did not mean. Real tokens live far below both marks — a few kilobytes is already more than most proxies expect in a header.
Common errors
Unexpected '}' at line 3, column 1 — probable cause: trailing comma after the last property.- The claims pane is scanned as JSON before anything is encoded, and a trailing comma is the usual cause — JSON forbids it where JavaScript object literals allow it. The line and column point at the closing brace, with a code frame beneath.
Claim "exp" is the string "1700003600" in the payload editor — NumericDate is a non-negative whole number of SECONDS since 1970-01-01 (RFC 7519 §2), never a string and never milliseconds; use 1700003600 for one hour from now.- Quoting a timestamp is easy to do and hard to see, and it fails quietly downstream: many verifiers treat a non-numeric exp as no expiry at all. The refusal names the claim, quotes the value and prints a correct number for the clock the page is holding.
ES256 needs a key with `kty` "EC" with `crv` "P-256", but this JWK has `kty` "RSA" — pick the algorithm that matches the key, or paste the right key.- The algorithm selector and the key must agree, and the mismatch is caught while the key is imported rather than at signing time. Both directions land here. The fix is whichever of the two you know to be right — the algorithm you meant, or the key you meant.
Payload is 70,008 characters, past the 65,536-character cap at character 65,537 — nothing is truncated and nothing is signed; trim the payload to 64 KB or fewer.- The counts are exact and the cap is a refusal: the pane is left alone, character 65,537 is named as the point the limit was crossed, and no partial token is produced. A claims set this large is usually a whole document pasted into the wrong pane.
FAQ
Is it safe to paste my signing key into this page?
The key is read by this tab's own code, used once by the browser's WebCrypto implementation, and dropped; it is absent from the shareable state by construction, absent from the downloadable state file, and never written to storage, so closing the tab ends it. What no page can do is vouch for the machine it runs on. Mint with test credentials whenever the difference does not matter.
Which algorithm should I pick?
If one service both mints and checks the token, an HS algorithm with a long random secret is the simplest thing that works. If anyone else has to check it, choose a public-key family so the checker never needs the signing key: RS256 is the most widely accepted, ES256 gives much shorter signatures, EdDSA is the modern choice where support exists, and PS256 is the RSA-PSS counterpart to RS256.
Why does the token change every time I sign with ES256?
Because ECDSA includes a fresh random value per signature, and RSA-PSS a random salt. The first two segments stay identical — same claims, same encoding — while the third differs, and every version verifies against the same public key. Do not test for determinism here: the HS and RS families and EdDSA repeat exactly, and the others are supposed not to.
Can I create a token with alg none?
No. The value is refused before anything is assembled, and the refusal explains itself. An unsecured JWS is a defined form, but it is also the shape of the best-known JWT attack — a forged header declaring no algorithm, handed to a verifier that believed it. ES256K, Ed448 and X448 are refused too, for the different reason that the platform does not implement them.
What does the jti helper generate?
Sixteen bytes drawn from the browser's cryptographic random source, written as base64url — long enough that collisions are not a practical concern. The claim exists so a verifier can recognise a token it has already seen and refuse the replay, which only helps if something on the other side records the identifiers.
Why does my token fail verification somewhere else?
In order of how often it turns out to be the cause: the secret was read in the wrong encoding, so the bytes differed; the verifier expects a different algorithm than the one stamped in the header; the key selected by kid was not the key the verifier looked up; or the token was re-serialised in transit. Compare the signing input shown here against what the verifier reports receiving.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Sign now, from anywhere in the tool.
- ⌘/Ctrl + C
- Copy the finished token while nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy the token even while a selection is active.
- ⌘/Ctrl + S
- Download the token as .txt instead of saving the page.
- Esc
- Re-mask the key text and the token.
- ? or ⌘/Ctrl + /
- Open the shortcut help dialog.