What Is a JWT Token? Structure, Claims and Signatures

Guilliano MolaireGuilliano Molaire12 min read

A JWT (JSON Web Token) is a compact, URL-safe string that carries claims about a user or client, such as who issued it, who it is for and when it expires. In its common signed form it has three Base64URL parts (header, payload and signature), and the signature lets an API confirm the token came from a trusted issuer and was not changed, without looking up a server-side session.

The format is defined in RFC 7519, and two common uses are OAuth 2.0 access tokens, which RFC 9068 profiles as JWTs, and OpenID Connect ID tokens, which OpenID Connect Core says are “represented as a JSON Web Token (JWT)”. A JWT can also be encrypted (RFC 7519 section 1 allows either form), but this post is about the signed form. It decodes one token part by part, explains the kinds of claims it can carry, compares the signing algorithms you will see, lists the checks an API must run beyond the signature, and ends with how an identity provider such as Keycloak issues these tokens.

Key takeaways

  • A JWT is a format, not a protocol. Specs built on it, such as OpenID Connect for ID tokens and RFC 9068 for access tokens, define what a token is for; JWT only defines how its contents are written and protected.
  • In a signed JWT, the payload is encoded, not encrypted. Anyone holding the token can read it, so never put secrets in it.
  • A valid signature is not enough. Your API also has to check iss, aud, exp and the algorithm, as RFC 8725 (JWT Best Current Practices) recommends.
  • Revoking a token at the issuer does not reach an API that only checks it locally: the Keycloak guide notes that already issued access tokens “remain valid until they expire”, which is why short lifetimes matter.

What does a JWT look like?

Here is an example token whose header names RS256. The three parts are separated by dots:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImsxIn0.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20vcmVhbG1zL2RlbW8iLCJzdWIiOiI3YzFlOWE1Mi0zZjBiLTRmN2UtOWQxYS0yYjZjOGU0ZjBhMTEiLCJhdWQiOiJvcmRlcnMtYXBpIiwiZXhwIjoxNzkyMjUyODAwLCJpYXQiOjE3OTIyNTI1MDAsInR5cCI6IkJlYXJlciIsInNjb3BlIjoib3JkZXJzOnJlYWQifQ.eNPfp8As3_q8BglgVxytKSPALmqr_2Y0-ounEZKkK63Ite0l6j_6LDa4bELygqPM_QlBzowA_chzJTLAfbl-8sdjDBQZdJCcmn3DAI8I0a8scRsQ-FoK0VxRprTbbBwtr6BqY8t2kZ_OENtZtNnlPg2UGe1ynPSzX2Ifu-J6P5SVVAEGYjy5TODkaAUN7ckY9Sn4tIx_ZifqeV1nmmdbvNi7jeuUOM63iPSTCilHJM3mULg1bsf_Q9sprmB2br3e3_uAGO8pUJEn0EuCLWJB57J1O52Bg0LrdBS1fDRBOCyzyLXaKN94jdwSKcN8_SJ6BNtVt6nRTNStqH7NvJUkfw

Base64URL is ordinary Base64 with - and _ in place of + and / (RFC 4648 section 5), and JWS drops the trailing = padding (RFC 7515 section 2), so the result can sit in a URL or an HTTP header without escaping. That layout, called the JWS Compact Serialization, comes from RFC 7515 section 7.1. You can decode the first two parts with any Base64URL decoder, or paste the token into our JWT Token Analyzer to see them side by side.

The header

The first part decodes to a small JSON object that describes how the token is protected:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "k1"
}

alg names the signing algorithm, typ names the media type of the token, and kid (key ID) tells the verifier which of the issuer’s keys signed it. An issuer can publish several keys at once, for example while rotating them, so the kid is how your API picks the right public key.

The payload

The second part is the claims set, the statements the issuer is making:

{
  "iss": "https://auth.example.com/realms/demo",
  "sub": "7c1e9a52-3f0b-4f7e-9d1a-2b6c8e4f0a11",
  "aud": "orders-api",
  "exp": 1792252800,
  "iat": 1792252500,
  "typ": "Bearer",
  "scope": "orders:read"
}

Read in plain words, iss says the server at auth.example.com issued this token. sub says it is about the user 7c1e9a52..., and aud says it is meant for the orders-api. The iat value 1792252500 is 15:55 UTC on 17 October 2026, and the exp value 1792252800 is five minutes later, at 16:00 UTC. The payload typ of Bearer is the value Keycloak’s TokenUtil.TOKEN_TYPE_BEARER constant uses for access tokens. Finally, scope lists the orders:read permission the issuer granted, which the API decides how to enforce. The times are Unix timestamps (seconds since 1 January 1970, UTC), which RFC 7519 section 2 calls NumericDate.

The signature

The third part is the signature. The issuer takes the exact text header.payload (the first two Base64URL strings joined by a dot), signs it with its private key, and Base64URL-encodes the result. RFC 7515 section 5.1 defines that signing input. Because the signature covers both the header and the payload, changing a single character in either one makes verification fail.

What are JWT claims?

A claim is one name and value pair in the payload. RFC 7519 section 4 sorts claim names into three groups, and knowing which group a claim belongs to tells you how much you can rely on its meaning.

Registered claims

These seven names are reserved by the RFC itself in section 4.1. None is mandatory at the JWT level, but protocols built on top of JWT require several of them: OpenID Connect Core, for example, requires iss, sub, aud, exp and iat in every ID token.

Claim Name What it means
iss Issuer Who created and signed the token, usually a URL
sub Subject Who the token is about, unique within that issuer or globally
aud Audience Who the token is meant for, a string or a list
exp Expiration time After this time the token must be rejected
nbf Not before Before this time the token must be rejected
iat Issued at When the token was created
jti JWT ID A unique ID, useful for detecting replays

The sub claim deserves a note of its own: section 4.1.2 only requires it to be unique within the issuer’s context or globally, so unless you know it is globally unique, the safe key for a user in your database is the pair of iss and sub. See also our post on the sub claim.

Public claims

Public claims are names meant to be shared across many systems. To avoid collisions, section 4.2 asks you to either register them in the IANA “JSON Web Token Claims” registry or use a collision-resistant name such as a URL you control. OpenID Connect claims like email, name and auth_time are listed in the IANA JWT claims registry, so each of those names has one registered meaning.

Private claims

Private claims are names that an issuer and its consumers agree on between themselves, defined in section 4.3. A tenant_id or a plan claim you add for your own API is a private claim. They are convenient, but they only mean what your two sides agree they mean, so document them and do not expect another system to understand them. In Keycloak you add claims like these with protocol mappers; for troubleshooting, see why a custom attribute is not in your token.

A good rule for all three groups is to keep the payload small. A client sends the whole token each time it presents it, for example in the Authorization: Bearer header described in RFC 6750 section 2.1, and every claim you add is readable by anyone who sees it.

How is a JWT signed: HS256 vs RS256 vs ES256?

The alg value names an algorithm from RFC 7518 (JSON Web Algorithms). This post compares three of them, which differ mainly in who holds which key.

Algorithm Type Keys RFC 7518 status Fits best when
HS256 HMAC with SHA-256 One shared secret signs and verifies Required One service issues and checks its own tokens
RS256 RSA signature with SHA-256 Private key signs, public key verifies Recommended Many APIs verify tokens from one issuer
ES256 ECDSA P-256 with SHA-256 Private key signs, public key verifies Recommended+ Same as RS256, with smaller keys and signatures

With HS256, every party that can verify a token can also create one, because it is the same secret. That is fine inside a single service, but when ten APIs need to check tokens from your identity provider, it means ten places hold a secret that can mint tokens for any user. RFC 7518 also requires an HMAC key at least as long as the hash output, which is 256 bits for HS256, in section 3.2, so a short human-chosen password is not a valid key.

With RS256 or ES256, only the issuer holds the private key. Your APIs fetch the public keys from the issuer’s JWKS (JSON Web Key Set) endpoint, a URL that serves the current keys as JSON, and they can verify tokens without ever being able to create one. That separation is a good reason for an identity provider to sign with an asymmetric algorithm, and Keycloak, for example, defaults to RS256 (more on that below). See also our explainer on JWKS.

How does an API verify a JWT?

A signature check proves that someone holding the right key produced the token. It does not prove the token was meant for your API, or that it is still current. RFC 7519 section 7.2 describes the mechanics, and RFC 8725 adds the checks that real attacks have shown to be necessary. A resource server (the API that receives the token) should do all of these:

  1. Pin the algorithm. Configure the list of accepted algorithms in your code and reject anything else, as RFC 8725 section 3.1 requires. Never let the token’s own alg header choose how it is checked, because RFC 8725 section 2.1 describes how that lets alg: none tokens and RSA-to-HMAC confusion attacks through. For a related case, see our write-up of CVE-2026-5430.
  2. Verify the signature with the key whose kid matches, taken from the issuer’s JWKS endpoint and cached.
  3. Check iss exactly against the issuer you trust (section 3.8). A token signed by some other issuer, even a valid one, is not yours to accept.
  4. Check aud contains your API’s identifier (section 3.9). Without this, a token issued for one service can be replayed against another service that trusts the same issuer.
  5. Require exp and check it and nbf against the current time. RFC 7519 section 4.1.4 allows a small leeway for clock skew, “usually no more than a few minutes”.
  6. Check the token type. RFC 8725 section 3.11 recommends explicit typing, and RFC 9068 defines at+jwt as the header typ for JWT access tokens so an API cannot be tricked into accepting an ID token as an access token. Keycloak instead puts a typ claim in the payload, using the TOKEN_TYPE_BEARER value Bearer for access tokens and the TOKEN_TYPE_ID value ID for ID tokens.

In practice you should not write this logic by hand. Libraries such as jose for Node.js, PyJWT for Python and Spring Security’s resource server offer options for these checks, which you have to set to your own expected values. A minimal Node.js check looks like this:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const ISSUER = 'https://auth.example.com/realms/demo';
const JWKS = createRemoteJWKSet(
  new URL(`${ISSUER}/protocol/openid-connect/certs`)
);

export async function verifyAccessToken(token) {
  const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    audience: 'orders-api',
    algorithms: ['RS256'],
    requiredClaims: ['exp'],
    clockTolerance: 30,
  });

  // Accept bearer access tokens only: RFC 9068 sets typ at+jwt in the
  // header, Keycloak sets typ Bearer in the payload and ID in ID tokens.
  const isAccessToken =
    protectedHeader.typ?.toLowerCase() === 'at+jwt' ||
    String(payload.typ).toLowerCase() === 'bearer';
  if (!isAccessToken) {
    throw new Error('Not an access token');
  }
  return payload;
}

Per the jose options reference, the issuer and audience options require and check iss and aud, requiredClaims makes exp mandatory, clockTolerance applies to exp and nbf, and algorithms limits the accepted alg values; the check after the call rejects a token that carries neither access-token typ marker, such as a Keycloak ID token. For more on this topic, see Keycloak token validation for APIs and how to verify a Keycloak-issued access token on the backend.

Identity management as a service, on open source

Skycloak does what Auth0 and Okta do, SSO, MFA, SCIM, audit logs and enterprise federation, on an open source core. Unlimited users and applications on every plan, no charge per monthly active user, and you can export and self-host whenever you want.

Access tokens, ID tokens and refresh tokens are different jobs

Because JWT is only a format, the same three-part string can play quite different roles, and your code has to tell them apart.

  • ID token. Defined by OpenID Connect Core 1.0 section 2, it tells the client application about the user’s authentication. The spec says it is represented as a JWT and that its aud must contain the client’s own client_id, so it is addressed to the client rather than to an API.
  • Access token. Defined by OAuth 2.0, it tells an API what the caller is allowed to do. OAuth does not require it to be a JWT, but it can be one, and RFC 9068 section 2.2 lists the claims a JWT access token must carry: iss, exp, aud, sub, client_id, iat and jti. RFC 9068 section 2.2.3 says the token should include a scope claim when the authorization request had a scope parameter; see also our post on OAuth scopes.
  • Refresh token. RFC 6749 section 1.5 describes it as a credential the client uses to obtain a new access token when the current one expires, says it is intended for use only with authorization servers and never sent to resource servers, and says the string is usually opaque to the client, so treat it as an opaque string even when the provider happens to use a JWT for it.

Related reading: ID token vs access token, access token vs refresh token and OpenID Connect explained for developers.

How does an identity provider like Keycloak issue JWTs?

When an identity provider issues your tokens, your APIs only need to verify them. Keycloak, which its project site describes as open source identity and access management, is one example, and it shows the pieces involved.

  • An issuer URL per realm. A Keycloak realm, which per the Server Administration Guide manages a set of users, credentials, roles and groups and is isolated from other realms, has its own issuer, of the form https://host/realms/realm-name, and that is the value your API should expect in iss.
  • A JWKS endpoint. The Keycloak Server Administration Guide lists /realms/realm-name/protocol/openid-connect/certs as the JSON Web Key Set containing the public keys used to verify tokens. Realm keys can be rotated, which is why your API should fetch keys by kid rather than copy one into configuration.
  • RS256 by default. Keycloak’s DEFAULT_SIGNATURE_ALGORITHM constant is RS256, and an administrator can change it with the Default Signature Algorithm setting on the realm’s Tokens tab.
  • Short-lived access tokens. The Access Token Lifespan setting in the realm’s session and token timeouts controls the lifetime of OIDC access tokens (implicit flow tokens have a separate setting), and a client’s Advanced settings can override it. See also our JWT token lifecycle guide.
  • Revocation, with limits. The guide’s endpoint list includes /realms/realm-name/protocol/openid-connect/revoke for RFC 7009 token revocation, and Keycloak also has a token introspection endpoint, and a not-before revocation policy an administrator can push to clients that use the Keycloak OIDC client adapter. An API that only checks signatures and claims locally never asks Keycloak about the token, so it cannot learn of a revocation that way; the guide gives one example, for disabled clients, whose already issued access tokens remain valid until they expire. See also our comparison of introspection vs local validation.

Keycloak also adds its own private claims, such as realm_access and resource_access for realm and client roles, which the guide describes under role protocol mappers.

Frequently asked questions

Is a JWT encrypted?

A signed JWT is not encrypted. Its header and payload are only Base64URL-encoded, so anyone who has the token can read every claim, and the signature only protects against changes. If you need the contents hidden, the JOSE family has a separate encrypted format, JWE (RFC 7516), but the simpler choice is usually to keep sensitive data out of the token entirely.

Can a JWT be revoked?

Not on its own. A signed JWT carries everything needed to accept it, so an API that validates only locally has no way to see a revocation and will accept it until exp. You can limit that window with short access token lifetimes, revoke the refresh token through an RFC 7009 endpoint, and, for operations where it matters, ask the issuer’s RFC 7662 introspection endpoint whether a token is still active. See also our JWT lifecycle guide.

Where should I store a JWT in the browser?

The OWASP HTML5 Security Cheat Sheet warns that “a single Cross Site Scripting can be used to steal all the data” in browser storage, so avoid keeping tokens in localStorage. The IETF draft on OAuth for browser-based applications discusses alternatives, including a backend for frontend that keeps the tokens on the server and uses an HttpOnly cookie with the browser, and in-memory token storage. See also our JWT best practices post.

With a classic session cookie, the OWASP Session Management Cheat Sheet says the meaning of the session ID “must be stored on the server side”, and logging out means invalidating that server-side session. A signed JWT carries the claims itself, so a service with the issuer’s public key can check it without a session lookup, but, as the revocation answer above explains, local checks alone will not see a revocation before exp.

Is “JWT token” the right term?

Strictly, the T in JWT already stands for token, so “JWT token” repeats itself. It still means the same thing as JWT. The RFC suggests pronouncing JWT as “jot”, in section 1.

What to do next

Take one of your own tokens and paste it into the JWT Token Analyzer to see its header, claims and expiry, and check whether it carries the iss, aud and exp your API relies on. Then read the token validation guide and confirm your API pins its algorithm and checks issuer, audience and expiry on every request.

Identity management as a service, on open source

Skycloak does what Auth0 and Okta do, SSO, MFA, SCIM, audit logs and enterprise federation, on an open source core. Unlimited users and applications on every plan, no charge per monthly active user, and you can export and self-host whenever you want.

Guilliano Molaire
Written by
Founder, Skycloak

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

© 2026 Skycloak. All Rights Reserved. Design by Yasser SolimanKeycloak™ is a trademark of the Linux Foundation. Skycloak is not affiliated with the Keycloak project.