By AndyPublished

JWT Signature Verification Guide

Signature verification is the process of confirming that a JWT was signed by a specific key and that its contents have not been modified since signing. It is the most critical step in JWT processing, a decoded but unverified token should never be trusted. This guide explains the mechanics of verification, the difference between symmetric and asymmetric approaches, and what verification can and cannot prove.

Why Verification Matters

A JWT payload is just Base64url-encoded JSON. Anyone can modify the payload, change a user ID, elevate a role, extend an expiry, and re-encode it into a new token. Without verification, your application has no way to distinguish a legitimate token issued by your auth server from a forged one.

Signature verification uses cryptography to bind the header and payload to a specific key. If even a single character in the header or payload changes, the signature check fails. This makes tampering detectable, but only if you actually run the check. The verification algorithm itself is defined in RFC 7515 (JSON Web Signature); the family of algorithms supported (HS256, RS256, ES256, PS256 and variants) is specified in RFC 7518 (JSON Web Algorithms).

Symmetric Verification: HMAC Algorithms

HMAC algorithms (HS256, HS384, HS512) use a single shared secret known to both the issuer and the verifier. The signing and verification processes are identical: compute HMAC-SHA{256|384|512} over the signing input using the shared secret, then compare the result to the token's signature.

// Verification pseudocode for HS256
expected_sig = HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  shared_secret
)
is_valid = constant_time_compare(expected_sig, token_signature)

The comparison must use a constant-time algorithm to prevent timing side-channel attacks that could leak information about the secret.

HS256 is only appropriate when the issuer and all verifiers trust each other completely, because any party with the secret can also issue new tokens. If your token is verified by multiple independent services, prefer an asymmetric algorithm.

Asymmetric Verification: RSA and ECDSA

Asymmetric algorithms (RS256/RS384/RS512, ES256/ES384/ES512, PS256/PS384/PS512) use a public/private key pair. The issuer signs with the private key. Verifiers only need the public key, which can be distributed freely.

// RS256 signing (by the auth server, uses private key)
signature = RSA-PKCS1v15-SHA256(signing_input, private_key)

// RS256 verification (by the API, uses public key only)
is_valid = RSA-PKCS1v15-SHA256-Verify(signing_input, signature, public_key)

This separation of concerns is the key advantage: API services need only the public key to verify tokens. They cannot issue new tokens because they never have access to the private key. This is why RS256 and ES256 are preferred in multi-service architectures.

RSA vs ECDSA

  • ·RSA (RS256/RS384/RS512): Based on the difficulty of factoring large integers. Key sizes of 2048–4096 bits are standard. Widely supported across all JWT libraries and platforms. Tokens are larger due to the RSA signature size.
  • ·ECDSA (ES256/ES384/ES512): Based on elliptic curve cryptography. Much shorter keys (256 bits for ES256 vs 2048 bits for RS256) providing equivalent security. Produces smaller signatures. Preferred in bandwidth-constrained environments.
  • ·RSA-PSS (PS256/PS384/PS512): A stronger RSA padding scheme that provides security proofs not available with PKCS#1 v1.5 (RS256). Preferred over RS256 for new systems when RSA is required.

JWKS Endpoints

A JSON Web Key Set (JWKS) endpoint is a URL that publishes an issuer's public keys in a standardized format. When your application needs to verify a token, it fetches the public key corresponding to the token's kid header from the JWKS endpoint.

// Example JWKS endpoint response
GET https://auth.example.com/.well-known/jwks.json

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "key-2024-01",
      "n": "...",   // RSA modulus
      "e": "AQAB"  // RSA exponent
    }
  ]
}

Best practices for JWKS:

  • ·Cache JWKS responses using the HTTP cache headers provided by the issuer
  • ·Refresh the cache when you receive a token with an unknown kid
  • ·Set a maximum cache age (e.g., 24 hours) to pick up key rotations in reasonable time
  • ·Only fetch JWKS over HTTPS with certificate validation enabled
  • ·Validate the issuer's URL before using its JWKS, never fetch keys from an attacker-controlled URL

Verification in Code: Library Examples

Always use a well-tested JWT library for server-side verification. Here are correct verification patterns for the most common platforms. Note the explicit algorithm restriction and claim validation options, these are critical for security.

Node.js, jsonwebtoken

const jwt = require('jsonwebtoken')

// RS256 with explicit algorithm restriction
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],       // never omit, prevents alg confusion
  issuer: 'https://auth.example.com/',
  audience: 'https://api.example.com',
})

// HS256 with explicit algorithm restriction
const payload = jwt.verify(token, sharedSecret, {
  algorithms: ['HS256'],
  issuer: 'https://auth.example.com/',
  audience: 'https://api.example.com',
})

Python, PyJWT

import jwt

# RS256 verification
payload = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],        # explicit list, never ["RS256", "HS256"]
    issuer="https://auth.example.com/",
    audience="https://api.example.com",
)

# HS256 verification
payload = jwt.decode(
    token,
    shared_secret,
    algorithms=["HS256"],
    issuer="https://auth.example.com/",
    audience="https://api.example.com",
)

Go, golang-jwt/jwt

import "github.com/golang-jwt/jwt/v5"

// RS256 verification
parser := jwt.NewParser(
    jwt.WithValidMethods([]string{"RS256"}),  // explicit
    jwt.WithIssuer("https://auth.example.com/"),
    jwt.WithAudience("https://api.example.com"),
)
token, err := parser.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    }
    return publicKey, nil
})

Key Rotation Without Downtime

Rotating signing keys is necessary for long-term security, but must be done carefully to avoid invalidating all active tokens at once. The safe procedure:

  • ·Step 1: Generate a new key pair. Assign it a new kid (e.g., key-2026-05).
  • ·Step 2: Add the new public key to your JWKS endpoint alongside the old key. Both keys are now published. Verifiers will cache both.
  • ·Step 3: Update your auth server to sign new tokens with the new private key. New tokens carry the new kid.
  • ·Step 4: Wait for all tokens signed with the old key to expire. The overlap period equals your maximum token lifetime.
  • ·Step 5: Remove the old public key from your JWKS endpoint. Destroy the old private key.

Verifiers handle this transparently: they look up the key by kid, so they automatically use whichever key the token was signed with, as long as it is present in the JWKS. Tokens signed with the old key continue to verify successfully during the overlap period.

What Verification Proves, and What It Doesn't

A successful signature verification proves:

  • ·The token was signed by a party holding the specific private key or shared secret
  • ·The header and payload have not been modified since signing

Verification does not prove:

  • ·The token has not expired (you must check exp explicitly)
  • ·The token was issued by the expected issuer (check iss)
  • ·The token is intended for your service (check aud)
  • ·The claims are semantically valid for your application's business logic
  • ·The user still has the roles or permissions stated in the claims (claims are a snapshot at issuance)
Full JWT processing requires: 1) verify signature, 2) validate iss, aud, exp, nbf, 3) apply application-specific claim logic. Steps 2 and 3 are equally important as step 1.

Why Always Verify Server-Side

Client-side verification (as this tool demonstrates) is valuable for debugging and inspection, but it must never be used for authorization decisions in production. A determined attacker can modify client-side JavaScript to bypass verification checks. Server-side verification in a trusted environment, a backend API, a gateway, a serverless function, is the only meaningful security boundary.

Always use a well-tested JWT library for server-side verification. Do not implement JWT verification from scratch. Libraries like jsonwebtoken (Node.js), PyJWT (Python), golang-jwt/jwt (Go), and nimbus-jose-jwt (Java) have been reviewed and audited by the security community.

Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder