How to Create and Sign a JWT
Decoding a JWT shows you what's inside one. Creating one is the other half: you choose how it will be signed, decide which claims it carries, and sign it with a key the receiver can check. Here's the whole process, and the mistakes that make a token unsafe.
What signing actually does
A signed JWT (strictly, a JWS) is three Base64URL segments joined by dots. The first two are your header and payload. The third is a signature computed over exactly this string:
base64url(header) + "." + base64url(payload)
Change a single character of the header or payload afterwards and the signature no longer matches, so the receiver rejects the token. Signing doesn't hide the contents, though. The payload is only encoded, so anyone can read it (see Base64 Is Not Encryption).
Step 1: choose an algorithm
The alg header decides what kind of key you need:
| Family | Algorithms | Signs with | Verifies with |
|---|---|---|---|
| HMAC | HS256, HS384, HS512 | Shared secret | The same secret |
| RSA | RS256, RS384, RS512 | RSA private key | RSA public key |
| RSA-PSS | PS256, PS384, PS512 | RSA private key | RSA public key |
| ECDSA | ES256, ES384, ES512 | EC private key | EC public key |
| EdDSA | EdDSA (Ed25519) | Ed25519 private key | Ed25519 public key |
Use HMAC when one service both issues and checks the token: it's simple and fast, but everyone who can verify can also forge. Use an asymmetric algorithm (ES256 and EdDSA give short keys and signatures; RS256 is the most widely supported) when other services or third parties must verify. They only ever get the public key.
Step 2: write the header
The header is a small JSON object. alg is required, and typ is conventionally JWT:
{
"alg": "ES256",
"typ": "JWT",
"kid": "2026-10-signing-key"
}
Add a kid (key ID) if you rotate keys or publish several in a JWK Set. The verifier uses it to pick the right public key.
Step 3: choose the claims
The payload holds claims. The registered ones have fixed meanings:
iss: who issued the token, e.g.https://auth.example.com.sub: who it's about, usually a user ID.aud: who it's for. Receivers should reject tokens meant for someone else.exp: expiry, as a Unix timestamp in seconds.iat/nbf: issued-at and not-before times.jti: a unique ID, useful for revocation lists or replay checks.
{
"iss": "https://auth.example.com",
"sub": "user_8412",
"aud": "orders-api",
"iat": 1791561600,
"exp": 1791565200,
"scope": "orders:read"
}
Keep the payload small and never put secrets in it. Passwords, API keys and personal data you wouldn't print in a log all stay out.
Step 4: sign it
For HMAC, the key is a secret byte string. It should be at least as long as the hash: 32 random bytes for HS256, 48 for HS384 and 64 for HS512. A short, guessable secret such as secret can be brute-forced offline from a single token.
For the asymmetric families, you sign with the private key, usually a PEM block (-----BEGIN PRIVATE KEY-----) or a private JWK, and hand out only the public key. ECDSA signatures in a JWT are the raw r || s values, not the DER structure many crypto libraries produce by default. That's a classic source of "invalid signature" errors when mixing libraries.
In code, use a maintained library rather than assembling tokens by hand. For example, with jose in Node.js:
import { SignJWT, importPKCS8 } from "jose";
const key = await importPKCS8(process.env.JWT_PRIVATE_KEY, "ES256");
const token = await new SignJWT({ scope: "orders:read" })
.setProtectedHeader({ alg: "ES256", kid: "2026-10-signing-key" })
.setIssuer("https://auth.example.com")
.setSubject("user_8412")
.setAudience("orders-api")
.setIssuedAt()
.setExpirationTime("1h")
.sign(key);
Step 5: verify what you made
Always check a new token the way its receiver will: decode it, confirm the claims, and verify the signature with the public key (or the secret). The receiver should also pin the expected algorithm, check exp, nbf, iss and aud, and reject anything else. How JWT Authentication Works covers that side of the exchange.
Mistakes that make tokens unsafe
alg: none. An unsigned token proves nothing. Never issue one, and configure verifiers to reject it.- Algorithm confusion. A verifier that trusts the token's
algcan be tricked into checking an RS256 token as HS256, using the public key as the HMAC secret. Pin the algorithm on the verifying side. - Weak HMAC secrets. Generate them randomly. Don't use a word or a password.
- No expiry, or a very long one. Stateless tokens can't easily be revoked, so keep
expshort and use refresh tokens for long sessions. - Production keys in tools. Use throwaway test keys when experimenting, even in a tool that runs locally.
Related
To read an existing token, see How to Decode a JWT. For the wider picture, see Web Security Essentials and Encoding vs Encryption vs Hashing.
Try it
Open the JWT tool's Encoder tab, pick an algorithm, and click Generate for a test secret or key pair. Edit the header and payload, add iat or exp with one click, and copy the signed token. Decode & verify it then checks the signature with the matching key. Everything runs in your browser, so tokens and keys are never uploaded.