JWT Generator

Build a real signed JWT from a header, payload and secret. HS256, HS384 and HS512, with exp and iat helpers, signed by Web Crypto on your device.

🌐 Español

🔒 Private by design: your text is processed locally in your browser and never uploaded to any server.

One box, four sections, and the marker lines that split them

A token needs three genuinely different pieces of text: a header object, a payload object and a secret. This page runs on the shared paste-in layout, which offers exactly one text area, so the sections are separated by marker lines instead:

=== HEADER ===
{"alg":"HS256","typ":"JWT"}
=== PAYLOAD ===
{"sub":"1234567890","name":"John Doe","iat":1516239022}
=== SECRET ===
your-256-bit-secret

Payload and secret are required. Header is optional, and leaving it out generates a minimal one naming the algorithm you chose. A fourth optional === SUBJECT === section writes the sub claim without you editing the payload JSON by hand, and it overwrites any sub already in there, since typing one is an explicit instruction.

Two details are worth knowing before they surprise you. Marker matching is case-insensitive and tolerates surrounding whitespace, and anything you type above the first marker is thrown away. If the same marker appears twice, the later section wins outright: the parser starts a fresh buffer each time it sees a marker, so the earlier content is discarded rather than concatenated.

Signing a token from scratch

  1. Paste your sections into the box using the marker lines above.
  2. Choose a Signing algorithm. HS256 (recommended) is the default and the one nearly every backend expects; HS384 and HS512 change only the digest size.
  3. Optionally pick a duration under Auto-add “exp” claim (expires in). It starts at Don’t add automatically, so no expiry is invented unless you ask for one.
  4. Leave Auto-add “iat” claim (issued at now) ticked to stamp the current time into the payload, or untick it if you want the payload to stand exactly as written.
  5. Click JWT Generator. The button takes its label from the page title, the way every paste-in tool on this site does.
  6. Copy the result. The output holds the full token first, then the header and payload JSON that were actually signed, then a short notes block.

The iat and exp helpers only ever fill a gap

Neither helper overwrites a claim you supplied. Issued-at is written only when the payload has no numeric iat. Expiry is written only when the payload has no numeric exp, and its base is your own iat when one exists, falling back to the current clock when it does not, so the pair stays internally consistent rather than drifting apart by however long you spent editing.

The subject section behaves differently on purpose, and it is the one thing here that does overwrite. Give it a value and it replaces sub unconditionally, because there is no reading of “I typed a subject” that means “unless the payload disagrees”. With a payload of {"sub":"old"}, a subject of new-user, the issued-at helper on and a fifteen-minute expiry, the signed payload comes out as subject new-user, iat at the current second and exp exactly 900 seconds later.

Reproducing the published reference token

Base64URL is not Base64, and the difference is where homegrown signing goes wrong: minus for plus, underscore for slash, and no padding at all. Get one of those three wrong and you get a token that looks entirely plausible and fails every real verifier.

So the pipeline was checked against a token whose exact value is published and quoted everywhere. Paste the classic demonstration header and payload with the secret your-256-bit-secret, leave every option at its default, and the output is that published token, signature segment included, closing with a notes block whose first two lines read:

=== NOTES ===
Algorithm: HMAC-SHA256 (HS256)
Secret key length: 19 bytes (UTF-8)

Note that the issued-at helper stays out of the way there, because that reference payload already carries its own iat. Vectors for the two longer digests, and for a secret and payload containing multi-byte UTF-8, were computed separately in Node and are pinned in the test file.

To convince yourself rather than take the claim on trust, decode the token with the JWT Decoder and check the round trip, then recompute the signature yourself in the HMAC Generator, using the token’s first two segments and the dot between them as the message. That page prints standard Base64, so apply the three Base64URL substitutions before comparing. Encoding a segment by hand with Base64 Encode needs the same adjustment, for the same reason.

HMAC only, and where that leaves you

Everything is signed with a shared secret through the browser’s native Web Crypto implementation, so both the party issuing and the party verifying need the same string. RS256 and ES256, where a private key signs and a public key verifies, are not offered.

Two more limits worth stating. This builds tokens, it never parses one back into editable fields, so amending an existing token means decoding it first, editing the claims and pasting them back here. And a token is only as trustworthy as the secret behind it, so a test secret should stay a test secret. Claims that want a unique identifier pair well with the UUID Generator, and an exact exp value is easiest to work out in the Unix Timestamp Converter.

Mistakes arrive as a report, not as a banner

The shared layout collapses any error thrown by a tool into one generic sentence, which for this tool would hide the only thing you needed to read. So the report builder never throws. Every failure comes back as ordinary output with a status line and a specific explanation: a missing payload section tells you which marker line to add and shows an example object, a missing secret names its own marker line and where the key goes on the next one, and malformed JSON is reported with the parser’s own complaint, which carries the character position whenever the engine supplies one. A header or payload that parses into an array rather than an object is refused explicitly too, since neither is a legal JWT segment.

See it in action

Screenshot of the JWT Generator tool with the sample input “{"sub":"sysfenix-demo","name":"Local Only","iat"…”, Signing algorithm set to HS256 (recommended), Auto-add "exp" claim (expires in) set to Don't add automatically
JWT Generator mid-process: the sample input “{"sub":"sysfenix-demo","name":"Local Only","iat"…”, Signing algorithm set to HS256 (recommended), Auto-add "exp" claim (expires in) set to Don't add automatically.
Screenshot of the JWT Generator result screen showing the generated output “=== JWT GENERATOR === Algorithm: HS256 Status: E…”
The finished result: the generated output “=== JWT GENERATOR === Algorithm: HS256 Status: E…”. The download link is a local blob URL — the file never leaves your device.

Frequently asked questions

How do three separate values fit into a single paste box?

Through marker lines. A line reading three equals signs, the section name and three more equals signs opens a section, and everything until the next marker belongs to it. PAYLOAD and SECRET are required, HEADER is optional, and SUBJECT is a convenience that writes the sub claim for you. Matching ignores case and tolerates stray spaces, and any text sitting above the first marker is discarded rather than guessed at.

I pasted my own header with a different alg value. Which one wins?

The dropdown, always. Your header object is kept intact, including extras such as a kid key identifier, but its alg field is overwritten with the algorithm you selected before anything is signed. That is deliberate rather than convenient. A header that advertises one algorithm while the signature was produced with another is a broken token, and a verifier reading the header would be misled by it.

Will the expiry helper trample an exp I set myself?

No. Both helpers only fill a gap. The issued-at helper writes a claim only when your payload has no numeric one, and the expiry helper writes one only when your payload has no numeric exp, taking your own issued-at value as its base when there is one and the current clock otherwise. Set an exact number in your own JSON and both helpers step aside, which is how you build a token that expired last week.

Are RS256 and ES256 available?

Not here, only the three HMAC variants in the dropdown. An asymmetric algorithm needs a private key, and a browser page that offers to generate signing key pairs for you invites people to use throwaway keys in places they should not. The HMAC path was checked against externally computed vectors before it shipped, and adding an RSA path without that same level of checking would have been worse than leaving it out.

How was the signing checked, rather than assumed correct?

By rebuilding a token whose value is published and widely quoted. Feeding this tool the classic demonstration header and payload for subject 1234567890 and the secret your-256-bit-secret, with the defaults left alone, reproduces that published token character for character, including its signature segment. Further vectors for the two longer digests and for multi-byte UTF-8 input were computed independently in Node and pinned in the test suite.

Why does the report show a byte count where I expected my secret?

Because a signing secret is a reusable credential and screenshots travel. The notes print only its UTF-8 length, which is enough to catch the two mistakes that actually happen, a secret truncated by a half-finished copy and a secret that landed in the wrong section. Whitespace around a section is trimmed away before the count is taken, so a stray newline never shows up in it. The secret itself is used to derive the key, then left in the page you typed it into.

Related tools