JWT Encoder, Sign HS256, RS256 and ES256 Tokens

Write the header and claims as JSON, pick an algorithm, and the signed token rebuilds as you type. Keys stay in this tab. The checks under the token flag an expired exp, a short HMAC secret, or a claim nobody should be reading.

JWT signing workspace

header.json ok
payload.json ok
signing key
0 bytes
token waiting
Signed token appears here
Open decoder
claims
ClaimValueReads as
No claims yet
checks
    HS256 0 bytes h 0 · p 0 · s 0 0 ms Signed locally, no network calls

    Three segments, and two of them are readable by anyone

    A JWT is base64url(header) . base64url(payload) . base64url(signature). The first two parts are plain JSON with a reversible encoding on top. Paste the header from the sample above into any Base64 decoder and you get {"alg":"HS256","typ":"JWT"} back, character for character. The encoder above produces exactly eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 for that header, the same string you see at the start of most tokens on the web.

    The signature is the only part needing a key. It covers the first two segments joined by a dot, so changing one character of the payload breaks verification. It does nothing to hide the payload. Treat every claim as public, because anyone holding the token, including the browser extension your user installed last week, reads it in one line of code.

    Two encoding details trip up hand-rolled encoders. Base64url swaps + and / for - and _ and drops the = padding. And the input to it is UTF-8 bytes, not a JavaScript string. Call btoa() on a payload holding "name": "Zoë" and the browser throws an InvalidCharacterError. This page encodes through TextEncoder first, so accented names and emoji in claims survive.

    Which alg belongs in your header

    The choice comes down to who signs and who verifies. Signature length is what you pay for it on every request.

    algSigning keyVerifying keySignature in tokenPick it when
    HS256Shared secretSame secret43 charsOne service issues and checks its own tokens
    RS256RSA private keyRSA public key342 chars (2048 bit)Several services verify, one issuer signs, broad library support needed
    PS256RSA private keyRSA public key342 charsSame as RS256 where a policy demands PSS padding (FAPI, some banks)
    ES256P-256 private keyP-256 public key86 charsAsymmetric signing with short tokens, mobile and IoT clients
    noneNoneNone0 charsUnit test fixtures. Never accepted by a server you run

    HS256 has one structural problem. Every service able to verify a token is also able to mint one, because both jobs use the same secret. Once a third service needs to check tokens, move to RS256 or ES256 and publish the public key at a JWKS endpoint. The verifiers then hold nothing worth stealing.

    Run the same payload through RS256 twice above and the token stays identical. Switch to ES256 or PS256 and the signature changes on every run. Both are randomized by design, and every one of those signatures verifies. Snapshot tests comparing a whole ES256 token against a stored string will fail for this reason, so compare decoded claims instead.

    Registered claims and the unit each one expects

    RFC 7519 defines seven claim names. None are mandatory in the spec, and most libraries enforce some of them by default anyway.

    exp, nbf, iat
    NumericDate, meaning whole seconds since 1970 UTC. Date.now() in JavaScript returns milliseconds, so exp: Date.now() + 3600 produces a token valid for roughly 56,000 years. Divide by 1000 and floor. The checks panel flags any timestamp past the year 5000.
    iss
    Who issued the token, usually a URL such as https://toolexe.com. Verifiers compare it as an exact string, so a trailing slash on one side and not the other causes a rejection.
    aud
    Who the token is for. A string or an array of strings. Libraries such as jose and PyJWT reject a token carrying aud when the verifier was not told which audience to expect.
    sub
    The principal, normally a stable user ID. Use an opaque ID instead of an email address, since emails change and end up in logs.
    jti
    A unique token ID. Useful only when the server stores it, for revocation lists or replay checks on one-time tokens.

    Everything else in the payload is a private claim. Keep those short. A token rides along on every request, and a cookie holding one is capped near 4 KB by browsers.

    Matching this page from your backend

    Here is the sample payload signed in three common libraries:

    // Node, jose
    import { SignJWT } from 'jose';
    const key = new TextEncoder().encode(process.env.JWT_SECRET);
    const token = await new SignJWT({ name: 'Leila Haddad', role: 'editor' })
      .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
      .setIssuer('https://toolexe.com')
      .setSubject('usr_4821')
      .setAudience('toolexe-dashboard')
      .setIssuedAt()
      .setExpirationTime('15m')
      .sign(key);
    # Python, PyJWT
    import jwt, time
    now = int(time.time())
    token = jwt.encode(
        {"iss": "https://toolexe.com", "sub": "usr_4821", "iat": now, "exp": now + 900},
        secret, algorithm="HS256")
    // PHP, firebase/php-jwt
    use Firebase\JWT\JWT;
    $token = JWT::encode(['sub' => 'usr_4821', 'iat' => time(), 'exp' => time() + 900], $secret, 'HS256');

    Expect the tokens to differ from this page even with identical claims and secret, and expect every one of them to verify. JSON key order and whitespace go straight into the signed bytes. php-jwt writes its header as {"typ":"JWT","alg":"HS256"}, typ first, so its tokens start with eyJ0eXAiOiJKV1Qi instead of eyJhbGciOi. This page keeps keys in the order you typed them and strips whitespace. Put typ first in header.json and the first segment matches php-jwt byte for byte.

    When the server answers 401

    1. Decode before you debug. Paste the rejected token into the JWT decoder and compare alg, iss and aud with what the verifier expects. A mismatch there explains most failures without touching keys.
    2. Check the clock. An iat or nbf a few seconds ahead of the server clock gets rejected by strict verifiers. Most libraries accept a leeway setting of 30 to 60 seconds.
    3. Check how the secret is read. A secret generated with openssl rand -base64 32 is 32 random bytes written as 44 characters. If the server decodes it and this page does not, the keys differ. Tick the base64url box to match a server reading decoded bytes.
    4. Check the key format. WebCrypto here and most modern libraries want PKCS#8 (BEGIN PRIVATE KEY). Older OpenSSL output is PKCS#1 (BEGIN RSA PRIVATE KEY). Convert with openssl pkcs8 -topk8 -nocrypt -in old.pem -out key.pem.
    5. Check the header transport. The header is Authorization: Bearer <token> with one space. A stray newline from a copied token, or quotes left around it in a shell variable, produce an invalid signature error with nothing wrong in the token itself.

    What this encoder does not cover

    • No encryption. Output is a signed JWS. For a payload hidden from the holder you need JWE (RFC 7516), which this page does not produce.
    • No EdDSA or ES512. Browser support for Ed25519 and P-521 in WebCrypto is still uneven, so both are left out instead of failing on some visitors.
    • PKCS#8 keys only. PKCS#1, SEC1 (BEGIN EC PRIVATE KEY), encrypted PEM and JWK input are rejected with a message. Convert with openssl first.
    • No verification step. This page signs. Checking a token against a key belongs in the decoder or in your own test suite.
    • Test keys, not production keys. Nothing leaves the tab, and you will see no request in the network panel while signing. A production secret pasted into any web page has still passed through your clipboard, so rotate one if it ends up here.

    JWT encoding questions from the support queue

    Rejected signatures, timestamp units, secret length, and when alg none shows up.

    Is a JWT encrypted?

    No. A standard JWT is signed, not encrypted. The header and payload are base64url text, readable by anyone holding the token. The signature proves the content was not changed after signing. If the payload has to stay hidden from the client, use JWE or keep the data on the server and put only an ID in the token.

    Why does my backend produce a different token from the same claims and secret?

    The signature covers exact bytes, so key order, whitespace and header field order all change the output. php-jwt puts typ before alg, Python dicts keep insertion order, and an iat taken one second apart changes everything after the first dot. ES256 and PS256 signatures are also randomized per run. Decode both tokens and compare claims instead of comparing strings.

    How long should an HS256 secret be?

    RFC 7518 section 3.2 requires a key at least as long as the hash output: 32 bytes for HS256, 48 for HS384, 64 for HS512. Use random bytes, not a phrase. openssl rand -base64 32 gives you one. Paste it here with the base64url box ticked so the page signs with the decoded bytes, matching a server reading it the same way.

    My exp claim shows a date thousands of years away. What went wrong?

    The value is in milliseconds. JWT timestamps are seconds since 1970, and Date.now() in JavaScript returns milliseconds. A token built that way never expires in practice. Use Math.floor(Date.now() / 1000) + ttl. The checks panel on this page flags any timestamp past the year 5000 for this reason.

    Should microservices use HS256 or RS256?

    RS256 or ES256 once more than one service verifies tokens. With HS256 every verifier holds the signing secret, so a breach of any one service lets an attacker mint tokens for all of them. With an asymmetric alg only the issuer holds the private key and the rest fetch the public key from a JWKS URL.

    Is alg none ever safe to accept?

    Not on a server. Several libraries in 2015 accepted tokens with alg none when the verifier did not pin an algorithm, letting anyone forge a token by deleting the signature. Current libraries reject it by default. Pin the expected alg in your verify call anyway. Unsigned tokens are fine as fixtures in unit tests.

    Where do I put the token in a request?

    In the Authorization header as Bearer followed by one space and the token. The Copy Authorization header button above produces that line. For browser sessions an HttpOnly cookie keeps the token away from page scripts, at the cost of needing CSRF protection on state changing requests.

    Does the page send my secret or private key anywhere?

    No. Signing runs through the WebCrypto API built into your browser, and the page makes no request while you type. Generated key pairs exist only in the tab and vanish on reload. Use throwaway keys for testing all the same, since a key pasted into any website has passed through your clipboard history.