JWT Encoder

Encode and sign JSON Web Tokens client-side

Development fixtures only. Use invented claims and a throwaway key. Do not paste production signing secrets, bearer tokens, API keys, or real identity data. This tool does not validate a production token design or manage key rotation.

The signing operation runs in this browser page. Use throwaway test material only.

How it works

  • • JWT consists of Header, Payload, and Signature separated by dots
  • • Header and Payload are Base64URL encoded JSON
  • • Signature is HMAC using the algorithm you selected
  • • The encoding and HMAC operation run in this browser page
  • • The result is a test fixture, not a production identity credential

When a hand-built token is the right tool

A verifier is only trustworthy if you have seen it say no. Real tokens are hard to produce in a bad state, so you build them: one whose exp is in the past, one whose nbf is in the future, one with the wrong aud or iss, one signed with the wrong key, and one signed with HS256 sent to an endpoint that expects an asymmetric algorithm. That last case is the algorithm-confusion test described in RFC 8725: a verifier must accept only the algorithms it expects, not whatever the token header names. This tool signs only HS256 and HS512 and always writes the algorithm you selected into the header, so it cannot make an alg: none token. Each of the other cases takes one edit of the payload shown below.

A token you can reproduce

Use the default header, paste this payload, set the algorithm to HS256 and use the synthetic secret below. The output must be byte for byte the token shown. If it is not, a field differs; JSON key order and whitespace matter because they change the signed bytes.

Inputs and output for the reproducible token
FieldValue
Secret (32 bytes, synthetic)synthetic-test-secret-key-32-byt
Header{"alg":"HS256","typ":"JWT"}
Payload{"iss":"https://issuer.test.example","aud":"https://api.test.example","sub":"test-user-123","iat":1700000000,"nbf":1700000000,"exp":1700003600}
TokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci50ZXN0LmV4YW1wbGUiLCJhdWQiOiJodHRwczovL2FwaS50ZXN0LmV4YW1wbGUiLCJzdWIiOiJ0ZXN0LXVzZXItMTIzIiwiaWF0IjoxNzAwMDAwMDAwLCJuYmYiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.HEW0mzlWVnHQAHy7V6Zs1mbzit6L0vCgasILJY5LvWI

Check the signature outside the browser. Both commands print HEW0mzlWVnHQAHy7V6Zs1mbzit6L0vCgasILJY5LvWI:

Bash with OpenSSL and GNU coreutils 8.31+ (basenc), and Node.js
TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci50ZXN0LmV4YW1wbGUiLCJhdWQiOiJodHRwczovL2FwaS50ZXN0LmV4YW1wbGUiLCJzdWIiOiJ0ZXN0LXVzZXItMTIzIiwiaWF0IjoxNzAwMDAwMDAwLCJuYmYiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.HEW0mzlWVnHQAHy7V6Zs1mbzit6L0vCgasILJY5LvWI'

printf '%s' "${TOKEN%.*}" \
  | openssl dgst -sha256 -hmac 'synthetic-test-secret-key-32-byt' -binary \
  | basenc --base64url | tr -d '='

node -e "const c=require('node:crypto');
const t=process.argv[1], i=t.slice(0,t.lastIndexOf('.'));
console.log(c.createHmac('sha256','synthetic-test-secret-key-32-byt').update(i).digest('base64url'))" "$TOKEN"

Claims: seconds, not milliseconds

RFC 7519 defines exp, nbf and iat as NumericDate values: seconds since 1970-01-01T00:00:00Z UTC, not milliseconds. The sample uses iat and nbf 1700000000 (2023-11-14T22:13:20.000Z) and exp 1700003600 (2023-11-14T23:13:20.000Z), a one-hour lifetime. In JavaScript, write Math.floor(Date.now() / 1000); a raw Date.now() is a thousand times too large.

Registered time claims and what a verifier should do
ClaimTo test rejectionRule (RFC 7519)
expSet it to a past second, such as 1699999999The current time must be before exp (section 4.1.4)
nbfSet it to a future secondThe current time must be at or after nbf (section 4.1.5)
iatSet it far in the futureInformational; apply your own age limit (section 4.1.6)

RFC 7519 allows a small leeway for clock skew; if your verifier has one, test just outside it, not far outside.

Key length for HS256

RFC 7518 section 3.2 says a key at least as large as the hash output must be used with HS256, which means 256 bits (32 bytes) or more. This encoder only requires a non-empty secret, so it will sign with a short one; do not read that as approval. Use at least 32 random bytes whenever you are testing a verifier's key handling, and "Generate Test Secret" fills a throwaway value.

Encoded is not encrypted

The first two segments are Base64URL-encoded JSON. Anyone holding the token can read them, with no key; the secret only lets a verifier detect changes. Do not put passwords, personal data or anything confidential in a payload. To see this, paste the token into the JWT decoder, which reads claims without verifying the signature. Our JWT vulnerabilities article covers the common implementation mistakes.

Use synthetic values only

Signing happens in this page, but a production signing key pasted into any web form can be exposed to browser extensions, screen shares or clipboard history. If a key has been pasted anywhere it should not be, rotate it. Create invented claims and a throwaway secret, and point them at a test environment.

Share this: