Sign HS256 JWT
Paste a header and payload, provide a secret, copy the signed JWT. Useful for fixture generation in tests. Don't sign production tokens here.
What this tool does
Given:
- A header object (
algis forced toHS256) - A payload object (your claims)
- A secret (UTF-8 string)
The tool produces a JWT in the form
base64url(header).base64url(payload).base64url(HMAC-SHA256(...)).
The signature is computed via the browser's Web Crypto API.
When to use the Sign tab
- Unit test fixtures. Generate a known JWT with specific claims for a test that mocks a real auth response.
- Integration test setup. Pre-generate a valid token to seed your test database or HTTP test client.
- Debugging your auth service. Compare a token your service produces to one this tool produces with the same input — if they differ, your service is doing something unexpected.
- Learning. Watching the JWT structure update as you tweak claims is the fastest way to internalize what each part does.
When NOT to use the Sign tab
- Real production signing. Don't paste a real secret here. Even though everything stays in your browser, copy-paste habits leak secrets to clipboard managers, AI chat assistants, or screenshots.
- RSA / ECDSA tokens. Use a server-side library; this tool is HMAC-only.
- Tokens for systems you don't control. If you're crafting a token to send to someone else's API, you'd need their secret — which they won't share. Their service signs the tokens.
What "alg=HS256 is forced" means
Even if you change "alg" in the header textarea, the
tool overwrites it to "HS256" before signing. This is a
safety measure: it prevents you from accidentally generating a
"none" token (which would bypass verification on
misconfigured servers) or a header that doesn't match the actual
signing operation.
Common claim shapes
Standard auth token
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"aud": "api.example.com",
"exp": 1735689600,
"iat": 1735603200,
"jti": "0e6f1b8c-2c33-4f1f-9c0b-2a3d4e5f6a7b"
} Refresh token
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"exp": 1740960000,
"iat": 1735603200,
"type": "refresh"
} OIDC ID token
{
"iss": "https://accounts.google.com",
"sub": "1234567890",
"aud": "your-client-id",
"exp": 1735689600,
"iat": 1735603200,
"email": "alice@example.com",
"email_verified": true,
"name": "Alice Example",
"nonce": "abc123"
} Programmatic equivalents
// Node.js with jose
import { SignJWT } from "jose";
const secretKey = new TextEncoder().encode(secret);
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.sign(secretKey);
// Python with PyJWT
import jwt
token = jwt.encode(payload, secret, algorithm="HS256")
// Go with golang-jwt
import "github.com/golang-jwt/jwt/v5"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, _ := token.SignedString([]byte(secret)) FAQ
Should I sign production JWTs with this tool?
No. Don't paste production secrets into web tools — even client-side ones. The Sign tab is for testing, fixtures, and learning. For real production signing, do it on a server you control with a library like jose (Node), PyJWT (Python), or github.com/golang-jwt/jwt (Go).
Why HS256 only?
HS256 is the simplest algorithm — single secret, no key import. RSA and ECDSA require generating or importing a key pair, which adds UI complexity. We may add RS256 / ES256 in a future update. For MVP, HS256 covers the test-fixture use case.
How long should my secret be?
For HS256, the secret should be at least 256 bits (32 bytes) of high-entropy data. A short string like secret is dangerous — easy to brute-force. In production, generate with crypto.randomBytes(32).toString('base64') or equivalent.
Why does the tool force alg=HS256?
Safety. If we let you set alg: none or anything else, you could accidentally generate a token that bypasses verification on a misconfigured server. Hard-pinning to HS256 keeps the tool predictable.