BytePane

JWT Generator: Create JSON Web Tokens for Testing

Security16 min read

The Test That Kept Failing

The integration test was straightforward: hit GET /api/orders with a valid admin token, expect a 200. But the test kept returning 401. The developer had hardcoded a JWT from jwt.io three weeks earlier — it had a 1-hour expiration. The fix was a one-liner: generate a fresh token programmatically with a far-future expiry, or better, generate one with a predictable structure your test suite controls. This guide covers both approaches.

Key Takeaways

  • JWT is defined by RFC 7519 — three Base64Url-encoded sections (header.payload.signature) joined by dots
  • Online JWT generators are fine for development testing only if they use the browser's Web Crypto API — inspect the network tab before trusting any tool with your secrets
  • For CI/test suites: generate tokens programmatically in test setup so expiration and claims are always controlled
  • ES256 (ECDSA P-256) produces 64-byte signatures — significantly smaller than RS256's ~256 bytes — making it the better choice for new asymmetric systems
  • Never generate JWTs for production in a browser tool — use server-side libraries with secrets from a vault or environment variables

What a JWT Generator Actually Needs to Do

A JSON Web Token (JWT) generator takes three inputs and produces a signed token string: a header (algorithm and token type), a payload (claims), and a signing key. The generator encodes the header and payload as Base64Url, concatenates them with a dot, then computes a cryptographic signature over that string using the specified algorithm. The full token is header.payload.signature.

According to the RFC 7519 specification (published May 2015 by IETF), a JWT must be compact (short enough to fit in a URL), URL-safe (no characters that need percent-encoding in query strings), and self-contained (the payload carries all claims needed for authorization without requiring a database lookup). These three properties drive the design of every JWT generator tool.

// Anatomy of a JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header (Base64Url)
.
eyJzdWIiOiJ1c2VyLTEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0NjgwMDAwMCwiZXhwIjoxNzQ2ODAzNjAwfQ
                                          ← Payload (Base64Url)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
                                          ← Signature (Base64Url)

// Decoded header:
{ "alg": "HS256", "typ": "JWT" }

// Decoded payload:
{
  "sub": "user-123",
  "role": "admin",
  "iat": 1746800000,   // issued at (Unix timestamp)
  "exp": 1746803600    // expires in 1 hour
}

The payload is Base64Url-encoded, not encrypted. Any JWT generator outputs a token that can be decoded by anyone without the signing key — the signature only proves that the token was issued by someone who held the secret, not that the contents are confidential. For encrypted JWTs, you need JWE (JSON Web Encryption, RFC 7516), which is a separate specification entirely.

To inspect any JWT you have generated, paste it into the JWT Decoder — it splits the three parts, decodes them, and shows you the exact claims without sending the token to any server.

Signing Algorithms: Which One to Choose

The signing algorithm is the most consequential decision when generating a JWT. The choice determines key management complexity, token size, verification performance, and cross-service architecture patterns.

AlgorithmTypeKey / Sig SizeSigning SpeedBest For
HS256Symmetric256-bit secret / ~43 charsFastestSingle-server apps, monoliths
HS384Symmetric384-bit secret / ~64 charsFastHigher-security HMAC needs
RS256Asymmetric (RSA)2048-bit key / ~256 bytes sigModerateOAuth2/OIDC, legacy JWKS
ES256Asymmetric (ECDSA)P-256 key / 64 bytes sigFastNew systems, mobile, edge
EdDSAAsymmetric (Ed25519)32-byte key / 64 bytes sigFastest asymmetricModern systems, Cloudflare Workers

The practical difference between RS256 and ES256 matters in high-throughput scenarios. Per cryptographic benchmark data, ES256 signs approximately 2–4× faster than RS256 while producing signatures one-quarter the size. For an auth server issuing thousands of tokens per second, this is meaningful. For a startup processing dozens per second, the difference is negligible — use whichever your team's preferred library supports best.

Generating JWTs Programmatically

For test suites and CI pipelines, generating tokens in code is far more reliable than copying from an online tool. Here are battle-tested patterns for the most common stacks.

Node.js with jsonwebtoken

// npm install jsonwebtoken
const jwt = require('jsonwebtoken');

// HS256: generate test tokens for Jest/Vitest test suite
function generateTestToken(overrides = {}) {
  const defaults = {
    sub: 'test-user-123',
    email: '[email protected]',
    role: 'user',
    iss: 'https://api.example.com',
    aud: 'https://app.example.com',
  };

  return jwt.sign(
    { ...defaults, ...overrides },
    process.env.JWT_SECRET || 'test-secret-do-not-use-in-production',
    { expiresIn: '1h', algorithm: 'HS256' }
  );
}

// Usage in tests:
const adminToken = generateTestToken({ role: 'admin', sub: 'admin-user-456' });
const expiredToken = generateTestToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
const noRoleToken = generateTestToken({ role: undefined });

// RS256: generate test tokens with key pair
const { generateKeyPairSync } = require('crypto');

const { privateKey, publicKey } = generateKeyPairSync('rsa', {
  modulusLength: 2048,
});

const rs256Token = jwt.sign(
  { sub: 'user-123', role: 'admin' },
  privateKey,
  { algorithm: 'RS256', expiresIn: '15m' }
);

// Verify with public key (simulates downstream service):
const decoded = jwt.verify(rs256Token, publicKey, {
  algorithms: ['RS256'],
});
console.log(decoded.sub); // 'user-123'

Python with PyJWT

# pip install pyjwt cryptography
import jwt
import time
from datetime import datetime, timedelta, timezone

SECRET = "test-secret-for-dev-only"

def generate_test_token(
    sub: str = "test-user-123",
    role: str = "user",
    expires_in_minutes: int = 60,
    extra_claims: dict = None,
) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": sub,
        "role": role,
        "iss": "https://api.example.com",
        "aud": "https://app.example.com",
        "iat": now,
        "exp": now + timedelta(minutes=expires_in_minutes),
        **(extra_claims or {}),
    }
    return jwt.encode(payload, SECRET, algorithm="HS256")

# Generate various test tokens:
valid_token = generate_test_token()
admin_token = generate_test_token(sub="admin-456", role="admin")
expired_token = generate_test_token(expires_in_minutes=-60)  # already expired

# ES256 with ECDSA key:
from cryptography.hazmat.primitives.asymmetric.ec import generate_private_key, SECP256R1
from cryptography.hazmat.backends import default_backend

private_key = generate_private_key(SECP256R1(), default_backend())
public_key = private_key.public_key()

es256_token = jwt.encode(
    {"sub": "user-789", "role": "viewer", "exp": time.time() + 3600},
    private_key,
    algorithm="ES256",
)

decoded = jwt.decode(es256_token, public_key, algorithms=["ES256"], audience=None)
print(decoded)  # {'sub': 'user-789', 'role': 'viewer', 'exp': ...}

Go with golang-jwt/jwt

// go get github.com/golang-jwt/jwt/v5
package main

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("test-secret-dev-only")

type Claims struct {
    Role  string `json:"role"`
    Email string `json:"email"`
    jwt.RegisteredClaims
}

func GenerateTestToken(userID, role, email string) (string, error) {
    claims := Claims{
        Role:  role,
        Email: email,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   userID,
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
            Issuer:    "https://api.example.com",
            Audience:  jwt.ClaimStrings{"https://app.example.com"},
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func GenerateExpiredToken(userID string) (string, error) {
    claims := Claims{
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   userID,
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // past
            IssuedAt:  jwt.NewNumericDate(time.Now().Add(-2 * time.Hour)),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

Using the Web Crypto API (Browser / Deno / Cloudflare Workers)

// Works in modern browsers, Deno, Bun, and Cloudflare Workers
// The Web Crypto API is the only correct way to generate JWTs client-side

async function generateJwt(payload, secretString) {
  const encoder = new TextEncoder();

  // Import the secret key
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secretString),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const header = { alg: 'HS256', typ: 'JWT' };
  const now = Math.floor(Date.now() / 1000);

  const fullPayload = {
    iat: now,
    exp: now + 3600,  // 1 hour
    ...payload,
  };

  const encodeBase64Url = (obj) =>
    btoa(JSON.stringify(obj))
      .replace(/=/g, '')
      .replace(/+/g, '-')
      .replace(///g, '_');

  const headerEncoded = encodeBase64Url(header);
  const payloadEncoded = encodeBase64Url(fullPayload);
  const signingInput = `${headerEncoded}.${payloadEncoded}`;

  const signature = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(signingInput)
  );

  const signatureEncoded = btoa(String.fromCharCode(...new Uint8Array(signature)))
    .replace(/=/g, '').replace(/+/g, '-').replace(///g, '_');

  return `${signingInput}.${signatureEncoded}`;
}

// Usage:
const token = await generateJwt(
  { sub: 'user-123', role: 'admin', email: '[email protected]' },
  'your-256-bit-secret'
);
console.log(token); // eyJhbGci...

The Web Crypto API has been available in all major browsers since 2014, Deno since v1.0, and Node.js since v15. Cloudflare Workers expose it as the global crypto object. For production use in Workers, prefer @tsndr/cloudflare-worker-jwt, which wraps this exact pattern.

Online JWT Generator Tools: An Honest Comparison

For ad hoc testing during development — crafting a one-off token to test a Postman collection, debugging claims parsing, or learning the JWT format — online generators are convenient. Here is how the main options compare, ranked by what matters most: security (does it hit a server?), algorithm support, and usability.

ToolServer-side?AlgorithmsCustom Claims UINotes
jwt.io debuggerClient-side JS but unclearHS256, RS256, RS512, ES256, PS256Inline JSON editorIndustry reference; verify tab verifies too
BytePane JWT GeneratorPure client-side (Web Crypto)HS256, HS384, HS512, RS256, ES256Form-based with presetsExpiry presets, copy-to-clipboard
dinochiesa.github.io/jwtPure client-sideHS256, RS256, ES256, PS256, EdDSAJSON editorStrongest algorithm coverage; open source
javainuse.comServer-side (backend call)HS256 onlyForm fieldsAvoid for real secrets — sends to server

The security-critical check: open DevTools Network tab, generate a token, and verify no POST request leaves the browser. Any tool that sends a network request while generating your JWT has received your signing secret. For development with throwaway secrets this is a minor risk; for staging or accidentally-real credentials it is a serious exposure. The open-source option at dinochiesa.github.io is auditable — you can read the source before trusting it.

Required Claims for Common Use Cases

RFC 7519 defines seven registered claim names. None are technically required by the spec itself, but authentication frameworks have strong expectations. Here are the claims you actually need, by use case.

Standard REST API Access Token

{
  "sub": "user-abc-123",       // REQUIRED: user identifier (your DB primary key)
  "iss": "https://auth.example.com",  // REQUIRED if you validate issuer
  "aud": "https://api.example.com",   // REQUIRED if you validate audience
  "iat": 1746800000,           // REQUIRED: when token was issued
  "exp": 1746803600,           // REQUIRED: expiry (iat + 3600 = 1 hour)
  "jti": "a4f2b1c3-...",       // RECOMMENDED: unique ID (replay protection)
  "scope": "read:orders write:orders"  // Custom: OAuth2-style scopes
}

OpenID Connect ID Token (OIDC)

{
  "iss": "https://accounts.example.com",  // REQUIRED by OIDC spec (OpenID Core 2.0)
  "sub": "user-abc-123",                  // REQUIRED: stable user identifier
  "aud": "my-client-id",                 // REQUIRED: OAuth2 client_id
  "exp": 1746803600,                      // REQUIRED
  "iat": 1746800000,                      // REQUIRED
  "auth_time": 1746799990,               // REQUIRED if max_age was in request
  "nonce": "rndm-str-from-auth-request", // REQUIRED if nonce sent in auth request
  "email": "[email protected]",           // Standard OIDC claim (profile scope)
  "email_verified": true,                // Standard OIDC claim
  "name": "Ada Lovelace"                 // Standard OIDC claim (profile scope)
}

M2M (Machine-to-Machine) Service Token

{
  "sub": "service-account:data-processor",  // service identifier, not user
  "iss": "https://internal-auth.example.com",
  "aud": ["https://billing-api.internal", "https://inventory-api.internal"],
  "iat": 1746800000,
  "exp": 1746886400,  // 24 hours: M2M tokens can live longer than user tokens
  "client_id": "data-processor-svc",  // OAuth2 client credentials flow
  "scope": "billing:read inventory:read inventory:write"
}

Generating Test Tokens in CI Pipelines

The most reliable approach for automated testing is never to copy a token from an online tool. Instead, generate tokens in your test setup with a well-known test secret that only exists in CI. This eliminates the "expired token" class of test failures entirely.

// vitest / jest setup file: test/setup.ts
import jwt from 'jsonwebtoken';

const TEST_JWT_SECRET = 'test-only-secret-not-for-production';

// Shared test token factory — import this in any test
export function createToken(
  claims: Partial<{
    sub: string;
    role: 'admin' | 'editor' | 'viewer';
    email: string;
    exp: number;
  }> = {}
): string {
  return jwt.sign(
    {
      sub: 'test-user-1',
      role: 'viewer',
      email: '[email protected]',
      iss: 'https://api.test',
      aud: 'https://app.test',
      ...claims,
    },
    TEST_JWT_SECRET,
    { algorithm: 'HS256', expiresIn: '1d' }
  );
}

// Token variants for common test scenarios:
export const tokens = {
  admin: createToken({ sub: 'admin-1', role: 'admin' }),
  editor: createToken({ sub: 'editor-1', role: 'editor' }),
  viewer: createToken({ sub: 'viewer-1', role: 'viewer' }),
  expired: createToken({ exp: Math.floor(Date.now() / 1000) - 3600 }),
  noSubject: jwt.sign({ role: 'viewer' }, TEST_JWT_SECRET),  // missing sub
};

// In your test:
it('returns 403 for viewer accessing admin endpoint', async () => {
  const res = await request(app)
    .get('/admin/users')
    .set('Authorization', `Bearer ${tokens.viewer}`);
  expect(res.status).toBe(403);
});

This pattern ensures every test run uses fresh, controlled tokens. The test secret is committed to the repository (it is only for testing) or passed as a CI environment variable. Your application reads JWT_SECRET from the environment — the same secret is injected in CI. For authentication method comparisons beyond JWT, see our full guide on API auth strategies.

Security Checklist for Generated JWTs

A JWT generator that silently produces insecure tokens will cause subtle production vulnerabilities. Before trusting any generated token, run through this checklist.

CheckWhy It MattersWhat to Verify
Algorithm specifiedPrevents "alg: none" attackHeader shows a real algorithm, not "none"
exp claim presentTokens without expiry live forever if leakedexp is set; confirm it is in the future
Secret strengthWeak HMAC secrets are brute-forceableAt least 256 bits (32+ bytes of entropy)
No sensitive dataPayload is readable by anyone with the tokenNo passwords, SSNs, or API keys in payload
iss and aud presentTokens accepted across services if not scopedIssuer and audience are specific values
Verifier checks algorithmAlgorithm confusion attackalgorithms: ['HS256'] in verify() call

For generating the cryptographically strong secrets that HS256 requires, use our Password Generator set to 64 characters with all character types enabled — this gives you ~380 bits of entropy, well above the 256-bit minimum. Understanding the underlying encoding: JWT uses Base64Url, a variant of standard Base64 with + replaced by - and / by _. Our Base64 encoder can help you experiment with the encoding step manually.

JWT Size Optimization

JWTs are sent with every API request. In a microservices architecture where a single user action triggers 5–10 internal service calls, token size multiplies. A bloated JWT with 20+ permission strings can exceed 2KB — noticeable in high-frequency mobile API calls.

// Bloated payload (bad): 1,200 bytes
{
  "sub": "user-123",
  "email": "[email protected]",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "role": "admin",
  "permissions": [
    "users:read", "users:write", "users:delete",
    "billing:read", "billing:write",
    "reports:read", "reports:write", "reports:export",
    "settings:read", "settings:write",
    "api-keys:read", "api-keys:write", "api-keys:delete",
    "webhooks:read", "webhooks:write", "webhooks:delete",
    "logs:read", "logs:export", "audit:read"
  ],
  "organization": { "id": "org-456", "name": "Acme Corp", "plan": "enterprise" },
  "metadata": { "createdAt": "2025-01-15T...", "lastLogin": "2026-05-09T..." }
}

// Lean payload (good): 180 bytes
{
  "sub": "user-123",
  "role": "admin",
  "org": "org-456",
  "iat": 1746800000,
  "exp": 1746803600
}
// Fetch full user profile from cache on first request, then cache for session

The lean-payload approach trades token completeness for token size. The application fetches the full user profile from Redis on the first authenticated request and caches it for the session duration. This is the pattern used by Auth0, Okta, and most large-scale auth providers — the token is an identity proof, not a user data store. Only include claims that the resource server absolutely cannot function without doing a database lookup to retrieve.

Frequently Asked Questions

Is it safe to generate JWTs in an online tool?

Safe for development only if the tool runs entirely client-side using the Web Crypto API and never sends your secret to a server. Inspect DevTools Network tab before trusting any tool. Never use online tools for production tokens — generate server-side with secrets from a vault or environment variables. Inspect the source or network traffic before trusting any online JWT generator with a real secret.

What is the difference between HS256, RS256, and ES256 for JWT signing?

HS256 uses one shared secret for both signing and verification — simple but requires distributing the secret to every verifier. RS256 uses RSA 2048-bit key pair — private key signs, public key verifies; ideal when many services verify but one issues. ES256 (ECDSA P-256) is asymmetric like RS256 but produces 64-byte signatures vs ~256 bytes for RS256, and signs faster. For new systems, ES256 or EdDSA are preferred.

What claims should I include when generating a JWT for testing?

Minimum: sub (user ID), iat (issued at), exp (expiration). For auth middleware testing: also iss (issuer) and aud (audience) matching exactly what your verification code expects. For RBAC: add role or permissions. For OIDC: iss, sub, aud, exp, iat are required by the OpenID Connect Core spec. Any other claims are custom and application-specific per RFC 7519.

How do I generate a JWT with an expiry in the past for testing expired token handling?

Set exp to a Unix timestamp in the past: exp: Math.floor(Date.now() / 1000) - 3600 gives a token that expired one hour ago. In Node.js jsonwebtoken: jwt.sign({ sub: 'user-1' }, secret, { expiresIn: '-1h' }). This tests your middleware's handling of TokenExpiredError versus JsonWebTokenError — two different error classes with different user-facing behaviors.

What is a JWT bearer token and how is it sent in API requests?

A bearer token grants access to whoever possesses it. JWTs are used as bearer tokens in the Authorization header: 'Authorization: Bearer <token>'. This scheme is defined in RFC 6750. The server extracts the token after 'Bearer ', verifies the signature, checks claims, and uses payload data — all without a database lookup. The stateless nature is why JWTs are preferred for distributed systems.

Can I use a JWT generator to test OAuth 2.0 and OpenID Connect flows?

For unit testing individual middleware: yes, generate tokens with the right claims. For testing full OAuth flows: use a local auth server (Keycloak, Dex, mock-oauth2-server). OIDC ID tokens require iss, sub, aud, exp, iat — and iss must match exactly what your client trusts. Hand-crafted tokens work for unit tests but miss the kid header claim that real JWKS-based verification requires.

What is the maximum size of a JWT and does it matter?

RFC 7519 sets no size limit, but HTTP header limits (~8KB in Apache/nginx) and cookie limits (4KB) are practical ceilings. Typical JWTs with 5–10 claims are 200–500 bytes. Bloated tokens with extensive permissions arrays can exceed 2KB. The rule: keep claims minimal, fetch extended user data from cache after verifying the token's identity claims.

Generate and Decode JWTs Instantly

BytePane's JWT tools run entirely in your browser — your secrets and payloads never leave your machine. Generate tokens with custom claims and expiration, or decode any JWT to inspect its structure.

Related Articles