JWT (JSON Web Token)

Security

A compact, URL-safe format (RFC 7519) for carrying a set of signed, or optionally encrypted, claims between two parties. Because the claims travel inside the token, an edge, API gateway or origin can verify the signature and the expiry locally, with no call back to the issuer.

Also known as JSON Web Token.

9 min read Updated Aug 30, 2026

Full Explanation

A JSON Web Token (JWT) is a compact, URL-safe format for carrying a set of claims between two parties. RFC 7519 defines it. It is a container format, not a protocol, not a transport and not a vendor feature. Something else decides what the claims mean and how the token travels. A JWT is not encryption by default. The usual signed form base64url-encodes its claims, so anyone holding the token can read them. Encryption is a separate, five-part JWE form that almost nobody uses at an edge. It is also not proof of identity by possession alone. A JWT presented as a bearer token can be used by whoever holds it. A party holding the right key can verify the token where it stands. That check needs no call back to the issuer. That is what makes JWTs interesting for a CDN. OpenID Connect ID Tokens are always JWTs. OAuth 2.0 access tokens are often JWTs, but they need not be. That is why RFC 9068 defines a separate profile for issuing them in JWT format. The acronym is pronounced "jot".

How it works

The claims set is a JSON object. It travels one of two ways. Inside a JWS structure, it is signed or MACed. Inside a JWE structure, it is encrypted. The number of dot-separated parts follows from that choice. A signed JWT has three segments. An encrypted one has five. The three-part signed form is what edge authentication means in practice. The order is header, payload, signature, each part base64url-encoded.

  • Header: names the algorithm in alg. Examples are HS256 for HMAC-SHA256, or RS256 and ES256 for asymmetric signing with RSA or ECDSA. It usually also carries kid, a key identifier. The verifier uses it to pick the right key out of a set.
  • Payload: the claims. RFC 7519 registers the names iss, sub, aud, exp, nbf, iat and jti. Use of every one of them is optional. So nothing in the format guarantees that a given token has an expiry at all.
  • Signature: computed by the issuer over the encoded header and payload joined by a period. It proves those bytes have not changed. It also proves they came from the holder of the signing key.

Validation is two separate jobs, and both matter. First comes the cryptography. Re-verify the signature with the key the verifier expects and the algorithm the verifier expects. Never trust only the algorithm the token names. RFC 7519 defines an Unsecured JWT. Its alg is "none" and its signature is the empty string. So a token that asks to be trusted without a signature is legal syntax. Second come the claims. RFC 7519 says a JWT "MUST NOT be accepted for processing" on or after its exp. The same applies before nbf. Implementers may allow a little leeway for clock skew. The RFC puts that at usually no more than a few minutes. With RS256 or ES256, the verifier needs only the issuer's public key. So an edge server can check tokens without ever holding a secret that could mint them.

Why it matters for a CDN

A JWT is self-contained. RFC 6749 describes an access token that may "self-contain the authorization information in a verifiable manner" rather than being a lookup handle. That is the whole point at the edge. The edge checks a signature with a key it already has. It checks the claims against the clock. Then it allows or rejects the request. There is no per-request database lookup and no round trip to an authorization server. This is what makes token authentication, API gateway policy and zero-trust gatekeeping affordable in the request path.

The same property is the cost. Offline verification means the edge sees only what was true when the token was signed. A subscription cancelled, a session logged out or an account disabled a minute ago is invisible to it until the token expires. Everything about operating JWTs at an edge follows from that trade.

What CDNs do

  • Akamai: EdgeWorkers is Akamai's edge function runtime. A jwt module for EdgeWorkers exposes a JWTValidator class. This class verifies signatures with imported CryptoKeys. You import it into your own code bundle from Akamai's EdgeWorkers examples repository. It is a library, not a switch on the platform. Read its defaults before trusting it. ignoreExpiration and ignoreNotBefore both default to true. So expiry and not-before are checked only if you explicitly set them to false. allowUnsecuredToken defaults to false. clockTolerance defaults to 60 seconds.
  • Cloudflare: Cloudflare Access signs a token for each allowed request. It sends the token to the origin in the Cf-Access-Jwt-Assertion request header. Browser requests also carry it as a CF_Authorization cookie. Cloudflare warns that this cookie is not guaranteed to be passed. The public keys sit at the team domain's /cdn-cgi/access/certs endpoint. Access rotates the signing key every six weeks by default. It keeps the previous key valid for seven days. So validators are told to match the token's kid against the published set, rather than hard-code a key.
  • Amazon CloudFront: its native signed URLs and signed cookies are not JWTs. You write a policy statement in JSON. You sign it with a private key. Its public half sits in a trusted key group. CloudFront verifies the signature. It supports RSA 2048 and ECDSA 256 signatures for these. Validating a real JWT at CloudFront's edge depends on the algorithm. The CloudFront Functions crypto module offers only hashing and HMAC helpers. Functions also have no network access. So CloudFront Functions can check an HS256 token. But it cannot verify RS256 or ES256, and it cannot fetch a key set. Asymmetric verification belongs in Lambda@Edge. Lambda@Edge runs Node.js or Python.

Watch out for

  • It is not a secret. A signed JWT hides nothing. So it must be protected in transit with TLS. It must not carry data that hurts if read. RFC 6750 goes further for bearer tokens. They "MUST NOT be stored in cookies that can be sent in the clear". Use JWE if the claims themselves need confidentiality.
  • Algorithm confusion. RFC 8725 records two live attacks against exactly this design. An attacker changes alg to "none". Some libraries trust that value. They validate the token without checking any signature. Or an attacker switches an RS256 header to HS256. Some libraries then verify an HMAC using the RSA public key as the shared secret. An attacker also has that key. The fix belongs in the verifier configuration, not in vigilance about the token.
  • Bearer semantics. A bearer token can be used by any party who has it. There is no proof of holding a key. mTLS and other proof-of-possession schemes add exactly that proof. A leaked JWT is a working credential for whoever has it.
  • Revocation is not free. Nothing in the format lets a verifier learn that a valid-looking token has been withdrawn. Getting that back means a deny-list at the edge, or an introspection call to the issuer. Both reintroduce the state and the round trip the token was chosen to avoid. Short lifetimes are the cheap answer. Instant revocation is not.
  • Expiry is only enforced if the verifier enforces it. The RFC obliges rejection past exp. But an expired token sails through a library with expiry checking off by default. So does a token issued without an exp at all. Test the expired case. Do not assume it.
  • Audience. Where one issuer can mint tokens for more than one relying party, RFC 8725 requires an aud claim. It also requires the recipient to validate that claim and reject on mismatch. That condition describes most CDN estates. One identity provider serves many properties. Without the check, a token minted for a low-value application is accepted by a high-value one.
  • Tokens can fragment the cache. A token is per-user. If it reaches the cache key, every user gets a private copy of a shared object. CloudFront, for one, keeps query strings, headers and cookies out of the cache key unless a cache policy adds them. AWS notes that including a value that does not change the response leads to caching duplicate objects. Authorize on the token. Then look up the cache without it.

Best practice

  • Configure the verifier with an explicit allow-list of algorithms. Confirm the header matches it, as RFC 8725 requires of libraries. Reject unsecured tokens outright.
  • Validate the claims, not just the signature. Check exp and nbf on every request. Check iss against the expected issuer. Check aud whenever one issuer serves more than one property. Allow only a few minutes of clock leeway.
  • Prefer RS256 or ES256, so each edge holds only a public key. Select the key by kid. Refresh the key set from the issuer's published endpoint, so a rotation does not turn into an outage.
  • Keep lifetimes short. RFC 6750 requires that a bearer token's lifetime be limited. It notes that short-lived tokens, one hour or less, reduce the impact of a leak. Pair short lifetimes with a refresh step, so users are not re-prompted.
  • Keep the payload to identity and authorization data. Keep the token out of the cache key. Carry it in an Authorization header rather than a query string. A query string would put it in logs and referrers.

Examples

# JWT structure (three base64url parts separated by dots)
# eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature

# Decode JWT payload (no verification)
echo 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ' | \
  base64 -d 2>/dev/null
# {"sub":"1234567890","name":"John"}

# Validate JWT in a Cloudflare Worker
async function validateJWT(token, publicKey) {
  const [headerB64, payloadB64, signatureB64] = token.split('.');
  const payload = JSON.parse(atob(payloadB64));

  // Check expiry
  if (payload.exp < Date.now() / 1000) {
    return null; // expired
  }

  // Verify signature with Web Crypto API
  const key = await crypto.subtle.importKey(
    'jwk', publicKey,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false, ['verify']
  );
  const valid = await crypto.subtle.verify(
    'RSASSA-PKCS1-v1_5', key,
    base64urlDecode(signatureB64),
    new TextEncoder().encode(headerB64 + '.' + payloadB64)
  );
  return valid ? payload : null;
}

# Nginx JWT validation (with njs module)
js_import jwt from conf.d/jwt.js;
js_set $jwt_valid jwt.validate;

server {
    location /api/ {
        if ($jwt_valid = "0") {
            return 401;
        }
        proxy_pass http://backend;
    }
}

# Generate a JWT for testing (Python)
python3 -c "
import jwt, time
token = jwt.encode(
    {'sub': 'user123', 'exp': int(time.time()) + 3600},
    'secret', algorithm='HS256'
)
print(token)
"

Frequently Asked Questions

A compact, URL-safe format (RFC 7519) for carrying a set of signed, or optionally encrypted, claims between two parties. Because the claims travel inside the token, an edge, API gateway or origin can verify the signature and the expiry locally, with no call back to the issuer.

# JWT structure (three base64url parts separated by dots)
# eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature

# Decode JWT payload (no verification)
echo 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ' | \
  base64 -d 2>/dev/null
# {"sub":"1234567890","name":"John"}

# Validate JWT in a Cloudflare Worker
async function validateJWT(token, publicKey) {
  const [headerB64, payloadB64, signatureB64] = token.split('.');
  const payload = JSON.parse(atob(payloadB64));

  // Check expiry
  if (payload.exp < Date.now() / 1000) {
    return null; // expired
  }

  // Verify signature with Web Crypto API
  const key = await crypto.subtle.importKey(
    'jwk', publicKey,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false, ['verify']
  );
  const valid = await crypto.subtle.verify(
    'RSASSA-PKCS1-v1_5', key,
    base64urlDecode(signatureB64),
    new TextEncoder().encode(headerB64 + '.' + payloadB64)
  );
  return valid ? payload : null;
}

# Nginx JWT validation (with njs module)
js_import jwt from conf.d/jwt.js;
js_set $jwt_valid jwt.validate;

server {
    location /api/ {
        if ($jwt_valid = "0") {
            return 401;
        }
        proxy_pass http://backend;
    }
}

# Generate a JWT for testing (Python)
python3 -c "
import jwt, time
token = jwt.encode(
    {'sub': 'user123', 'exp': int(time.time()) + 3600},
    'secret', algorithm='HS256'
)
print(token)
"

Yes. JWT (JSON Web Token) is also known as JSON Web Token. A compact, URL-safe format (RFC 7519) for carrying a set of signed, or optionally encrypted, claims between two parties. Because the claims travel inside the token, an edge, API gateway or origin can verify the signature and the expiry locally, with no call back to the issuer.

Related CDN concepts include:

  • TLS (Transport Layer Security) (TLS) — TLS (Transport Layer Security) is the protocol that turns a plain byte stream into a …
  • Token Authentication — Token authentication gates CDN-delivered content with a signed, expiring URL, cookie or header. Your application …
  • Zero Trust — Zero trust is a security model that grants no implicit trust: every request is authenticated …
  • API Gateway — An API gateway is the single front door for API traffic: a reverse proxy that …