DevKitHub

API & Security

JWT Generator — Create and Sign a JSON Web Token

Write a header and a payload, choose an algorithm and sign. HMAC tokens use your secret; RSA and ECDSA tokens use a PEM private key or a key pair generated in your browser, whose public half you can take elsewhere to verify the token.

4 lines
5 lines
public demo secret — replace it
HS2561 line
  • There is no exp claim, so nothing in the token makes it stop being accepted. Add one with the Expires in helper.

Signed in this page; the secret and keys are not sent anywhere. A JWT is signed, not encrypted — anyone who holds it can read the payload. Check an HS256 token in the JWT verifier, or read any token’s claims in the JWT decoder.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

A signed JWT is three base64url segments joined by dots: the header, the payload, and a signature over the first two exactly as encoded (RFC 7515). So what is signed is the compact JSON this page writes, not the text in the editors: whitespace is dropped and key order is kept. The header’s alg is set to the algorithm you choose, with a note if it said something else. alg "none" is refused, because an unsigned token is the original JWT vulnerability; to test that a server rejects one, the header and payload in base64url followed by a dot and nothing else is all it takes. A claim that appears twice is refused too, since JSON.parse keeps only the last and RFC 7519 requires names to be unique, and integers too large for JavaScript are flagged. HS256 output is checked against RFC 7515 Appendix A.1, RS256 against Appendix A.2, and HS256 tokens against this site’s own verifier.

HS256, HS384 and HS512 are HMAC with a shared secret, computed on this page. RFC 7518 §3.2 requires the key to be at least as long as the hash — 32 bytes for HS256, 64 for HS512 — and a shorter one is flagged, because it can be brute-forced offline from any single token. Anyone who can verify an HS token can also mint one. RS256 (RSASSA-PKCS1-v1_5, deterministic), PS256 (RSASSA-PSS with a 32-byte salt, so each signature differs) and ES256 (ECDSA on P-256) use the browser’s Web Crypto with a PKCS#8 private key; a PKCS#1 or SEC 1 key is refused with the openssl command that converts it. An ES256 signature must be the 64-byte R and S concatenated (RFC 7518 §3.4), which Web Crypto produces, and not the DER structure openssl dgst writes — the usual reason a hand-signed ES256 token fails elsewhere. Generate key pair makes RSA 2048 or P-256 keys in the page and shows the public key as PEM and as a JWK.

iat, nbf and exp are NumericDates: seconds since 1970, not the milliseconds Date.now() returns. The helpers write iat as now and exp as iat plus a duration such as 15m, 1h30m, 7d or PT1H; a bare number is read as seconds, although jsonwebtoken reads the string "120" as 120 milliseconds. Milliseconds, a missing exp and claims named like passwords or keys are all flagged, because a JWT is signed, not encrypted — anyone holding it can read the payload. Secrets and keys never leave the page; a browser test checks that no request carries them. Not supported: encrypted tokens (JWE), ES384, ES512 and EdDSA.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

The payload is not valid JSON: Expected double-quoted property name

{
  "sub": "1234567890",
}
Why:
A trailing comma after the last claim. JavaScript object literals allow one; JSON does not.
Fix:
Delete the comma before the closing brace. The error gives its line and column.

The payload has the claim "admin" twice.

{"sub":"alice","admin":false,"admin":true}
Why:
JSON.parse keeps only the last of two identical keys, so most libraries would sign admin: true while the text above it also says false. RFC 7519 requires claim names to be unique.
Fix:
Remove one of them. This generator refuses rather than guess which one was meant.

The payload is an array, but a JWT claims set must be a JSON object (RFC 7519 §4).

["sub","1234567890"]
Why:
RFC 7519 defines the claims set as a JSON object, so that every claim has a name. An array is valid JSON and a valid JWS payload, but not a JWT.
Fix:
Put the values in an object with named claims, such as {"sub": "1234567890"}.

The header says "alg": "none", which is an unsigned token, and this generator will not make one.

Why:
An unsigned token carries no proof of who wrote it. Libraries that once trusted alg none accepted tokens anyone could forge, and it remains one of the first things a security test tries.
Fix:
Pick a real algorithm. To test that your server rejects unsigned tokens, build one by hand: base64url header, a dot, base64url payload, and a trailing dot.

The secret is 20 bytes. RFC 7518 §3.2 requires a key at least as long as the hash output.

Why:
An HS256 token signed with a short secret can be brute-forced offline: anyone holding one token can test guesses against it without ever contacting the server.
Fix:
Use at least 32 random bytes for HS256, 48 for HS384 and 64 for HS512 — the random string generator makes one.

This is a PKCS#1 RSA key (BEGIN RSA PRIVATE KEY). Browsers import only PKCS#8.

Why:
Older OpenSSL versions and many tutorials write RSA keys as PKCS#1, but Web Crypto imports private keys only as PKCS#8, which begins -----BEGIN PRIVATE KEY-----.
Fix:
Convert it without changing the key: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key-pkcs8.pem

An ES256 token signed with OpenSSL fails verification in every library.

Why:
openssl dgst -sign writes an ECDSA signature as DER, 70 to 72 bytes, while RFC 7518 §3.4 requires the 64 bytes of R and S concatenated.
Fix:
Convert the DER signature to R and S, or sign with a JOSE library. ES256 signatures made here are already 64 bytes.

The token says it expires in the year 56000.

Why:
exp was written from Date.now(), which counts milliseconds, while NumericDate counts seconds, so the value is a thousand times too large.
Fix:
Use Math.floor(Date.now() / 1000), or the Expires in helper here. The generator warns when a time claim looks like milliseconds.

Frequently asked questions

Is it safe to sign a JWT with my real secret here?
The token is signed in your browser and there is no endpoint on this site that accepts a secret or a key; a browser test checks that no request made while the tool is used contains the secret. For production secrets the safest habit is still not to paste them anywhere — generate a test secret or key pair here instead.
Should I use HS256 or RS256?
HS256 when the same service issues and checks tokens: it is fast and simple, but every verifier holds the secret and can therefore mint tokens. RS256, PS256 or ES256 when others must verify: they get only the public key. ES256 tokens are much shorter than RSA ones.
Why will it not create an alg none token?
An unsigned token proves nothing, and a server that accepts one accepts tokens anyone can write. Refusing keeps this page from producing tokens that get accepted by mistake. For a negative test, an unsigned token is just the base64url header and payload joined by a dot, with a trailing dot.
How do I verify the token somewhere else?
For HS tokens, give the verifier the same secret. For RS256, PS256 and ES256, give it the public key shown after signing, as PEM or as a JWK — the JWK can go straight into a JWKS document. This site’s JWT verifier checks HS256, and the JWT decoder shows any token’s claims.

Last updated