Ask ten developers what a JWT is and you’ll hear ten similar answers: “it’s an encrypted token that holds the user’s session.” The first half is almost always wrong. Orbiting the JWT is a constellation of acronyms — JOSE, JWS, JWE, JWA, JWK, JWKS — and the confusion among them produces everything from harmless misunderstandings (“I decoded the token”) to serious design mistakes (“I put the password in the payload because it’s encrypted”). The alphabet soup exists for a good reason: each piece solves a distinct problem. The trouble is that, in practice, they all show up together, embedded in the same eyJ... string, and nobody stops to pull them apart.

This article is the mental map you’ve been missing. We’re not going to attack anything here — it’s a reference for developers who need to understand the ecosystem before implementing authentication, and for security professionals who want the right model in their heads before they audit. The through-line is a single truth that dispels the most common myth: a typical JWT is not encrypted, it’s signed. Signing and encryption are different things, solved by different specs, with different guarantees. We’ll walk through each one, see how they fit together, and finish with a clear guide on when to use what. To move from theory to the actual fields, the JWT / JOSE Attack Lab decodes, edits and re-signs tokens right in the browser.

JOSE: the umbrella that ties it all together

JOSE stands for JSON Object Signing and Encryption, and it’s the name of the IETF family of specifications that standardizes how to represent keys, signatures, and encryption using JSON. When someone says “JWT,” they’re actually invoking half of JOSE without realizing it. The five core specs were published together in 2015 and reference one another — you can’t really understand one without the others.

The division of labor is clean: one spec defines the data container (JWT), two define the protection formats (JWS for signing, JWE for encrypting), one defines the catalog of algorithms those formats can use (JWA), and one defines how to represent keys (JWK). All in JSON, all serializable into a compact, URL-safe string.

AcronymWhat it doesRFC
JWTJSON Web Token: a container of claims (assertions about an entity). Always serialized as a JWS or a JWE.RFC 7519
JWSJSON Web Signature: signs (or applies a MAC to) a payload. Guarantees integrity and authenticity.RFC 7515
JWEJSON Web Encryption: encrypts a payload. Guarantees confidentiality (and integrity of the content).RFC 7516
JWKJSON Web Key: represents a cryptographic key in JSON. A collection of them is a JWK Set (JWKS).RFC 7517
JWAJSON Web Algorithms: registry of algorithms for JWS and JWE (the alg and enc values).RFC 7518

There’s also a document you should know about even though it isn’t one of the five core specs: RFC 8725, the JSON Web Token Best Current Practices (BCP 225). It consolidates the security recommendations learned from years of botched implementations. If you’re going to read only one document from the set, read this one — right after this article.

JWT: the claims container

RFC 7519 defines the JWT as a compact, URL-safe means of transmitting claims between two parties. A claim is simply a key-value pair — an assertion about an entity. The JWT is not, in itself, an encryption or signing format; it is the content, the JSON holding the claims. Protection comes from the wrapper: JWS or JWE.

Claims fall into three groups. Registered claims are standardized and have reserved short names:

  • iss (issuer): who issued the token.
  • sub (subject): who the token is about (typically the user ID).
  • aud (audience): who the token is intended for (which API/service should accept it).
  • exp (expiration time): a NumericDate (seconds since the Unix epoch) after which the token is no longer valid.
  • nbf (not before): a NumericDate before which the token is not yet valid.
  • iat (issued at): when it was issued.
  • jti (JWT ID): a unique identifier for the token, useful for revocation and replay mitigation.

Beyond those, there are public claims (registered with IANA or namespaced by URI to avoid collisions) and private claims (agreed upon between the parties, such as role, tenant_id, email).

The point that organizes everything else: a JWT is always serialized as either a JWS or a JWE. There is no such thing as a “raw JWT” on the wire. When you receive that string with dots in it, it’s already a JWS or a JWE carrying a JWT as its payload. And in the overwhelming majority of cases — OIDC tokens, API tokens, stateless sessions — it’s a JWS, signed and not encrypted.

JWS: signature = integrity and authenticity

RFC 7515 defines the JWS, the format that signs a payload. The guarantee it offers is twofold: integrity (the content wasn’t altered after it was signed) and authenticity (it really was the key holder who issued it). What it does not offer is confidentiality — and this is exactly where misunderstanding number one lives.

In the compact serialization (the common form), a JWS has exactly three parts separated by dots:

BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)
eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZSJ9.eyJzdWIiOiI0MiJ9.NHVtX2Z4a2g...
└─────────── header ───────────┘ └─ payload ─┘ └── signature ───┘

The first two parts are just base64url of a JSON object. There’s no encryption: anyone who intercepts the token decodes and reads the header and the payload. The third part is the signature, computed over BASE64URL(header) . BASE64URL(payload) with the key and algorithm indicated in alg.

Repeat it to yourself: the payload of a JWS is readable by anyone. Decoding is a public operation, no key required. That’s why the golden rule is: never put a secret in the payload of a JWS. Passwords, card numbers, third-party API tokens — none of it. If you need to hide the content, you need JWE.

There’s also the JSON serialization, more verbose, which allows multiple signatures over the same payload — useful in scenarios where several parties need to attest to the same document. But in the life of an API or OIDC, it’s three-part compact serialization the whole time.

JWE: encryption = confidentiality

RFC 7516 defines the JWE, the format that actually encrypts the payload. The guarantee here is confidentiality, plus integrity of the content — JWE uses authenticated encryption (AEAD), so the content is unreadable to anyone without the key, and any tampering with the ciphertext or the protected header is detected when the tag is verified.

In the compact serialization, a JWE has exactly five parts separated by dots:

BASE64URL(protected header) . BASE64URL(encrypted key) . BASE64URL(iv) . BASE64URL(ciphertext) . BASE64URL(authentication tag)

The mechanics behind these five parts is hybrid encryption: a random Content Encryption Key (CEK) encrypts the content symmetrically (fast), and that CEK is then protected — encrypted or derived — from the recipient’s key, according to the algorithm indicated in alg. The five parts map directly onto this: the protected header describes the algorithms (and serves as additional authenticated data), the encrypted key is the protected CEK, the iv is the initialization vector, the ciphertext is the encrypted content, and the authentication tag validates the ciphertext and the protected header.

When should you use JWE? Whenever the token has to carry sensitive data that travels through untrusted parties or ends up stored in inspectable places — think tokens that pass through intermediaries, or that contain PII that shouldn’t leak even to the client storing them. The cost is higher: more complexity and more implementation pitfalls. In practice, the number of parts is the quickest way to identify the format in the middle of traffic: if you see three parts, it’s a JWS; five, it’s a JWE.

JWA: the algorithm catalog

RFC 7518 is the registry — the catalog that says which alg and enc values are valid and what each one means. JWA doesn’t “do” anything on its own; it’s the reference that JWS and JWE consult. The most important distinction, and the most confused, is between the two fields:

  • alg — present in both JWS and JWE. In a JWS, it identifies the signature or MAC algorithm. In a JWE, it identifies the key management algorithm (how the CEK is protected). In both cases it lives in the header.
  • enc — present only in JWE. It identifies the content encryption algorithm (how the CEK encrypts the payload). A JWS never has enc.

Typical alg values for signing/MAC (JWS):

  • HS256 — HMAC with SHA-256. Symmetric: the same secret generates and verifies the tag (technically a MAC, not a signature).
  • RS256 — RSASSA-PKCS1-v1_5 with SHA-256. Asymmetric: the private key signs, the public key verifies.
  • PS256 — RSASSA-PSS with SHA-256. A more modern RSA signature variant.
  • ES256 — ECDSA with the P-256 curve and SHA-256. Asymmetric, with smaller signatures.

Typical alg values for key management (JWE): RSA-OAEP, ECDH-ES, dir (direct: uses an already-shared symmetric key directly as the CEK, without encrypting any key — in this case the encrypted key part is empty).

Typical enc values (JWE only): A128GCM, A256GCM (AES in GCM mode), A128CBC-HS256 (AES-CBC combined with HMAC-SHA-256).

Don’t mix up the fields in your head. RS256 is a JWS signing alg; A256GCM is a JWE content enc. A token signed with RS256 and encrypted with an enc like A256GCM is a nested JWE (we’ll get to that), not an “either/or” choice.

JWK and JWKS: representing the key

RFC 7517 solves a practical problem: how to publish and reference cryptographic keys in JSON. A JWK is a JSON object that describes a key. The common fields:

  • kty (key type): RSA, EC, oct (symmetric/octet).
  • kid (key ID): the key’s identifier, used to match against the token header.
  • use: sig (signature) or enc (encryption).
  • alg: the intended algorithm for the key.

Each type has its own parameters: RSA uses n (modulus) and e (public exponent); EC uses crv, x, y; a symmetric key uses k. An RSA public key looks like this:

{
  "kty": "RSA",
  "use": "sig",
  "kid": "2ae",
  "alg": "RS256",
  "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
  "e": "AQAB"
}

Several keys together form a JWK Set (JWKS): a JSON object with a keys array. This is how an identity provider publishes its public verification keys, typically at the /.well-known/jwks.json endpoint:

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "2ae",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "9f1",
      "alg": "RS256",
      "n": "qDi7Tx4DhNvPQsl1ofxxc2ePQFcs-L0mXYo6TGS64CY_2vmrk...",
      "e": "AQAB"
    }
  ]
}

The final link is the kid. When a JWS arrives, the verifier reads the kid from the token header, looks up the key in the JWKS whose kid matches, and uses its n/e to verify the signature. The kid is what enables key rotation without breaking old tokens: the JWKS can publish the new key and the old one at the same time, with each token pointing to its own. (Be careful: the kid is a field controlled by whoever builds the token; it serves to select a key within a trusted set, never to blindly trust a key or path that the issuer points to.)

Putting it all together: the anatomy of a real token

Take any OIDC access token or ID token — the kind you get when you log in via Google, Auth0, Keycloak, or Cognito. It has three parts (so it’s a JWS):

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjJhZSJ9.eyJpc3MiOiJ...

Decoding the first part (the header):

{ "alg": "RS256", "typ": "JWT", "kid": "2ae" }

And the second (the payload, the JWT proper):

{
  "iss": "https://login.example.com",
  "sub": "auth0|64f1c0",
  "aud": "https://api.example.com",
  "exp": 1769904000,
  "iat": 1769900400,
  "scope": "openid profile email"
}

Now watch the pieces snap together, all in the same string:

  • It’s a JWT (RFC 7519): the payload is the claims container — iss, sub, aud, exp, iat.
  • Serialized as a JWS (RFC 7515): three parts, a signed and readable payload.
  • With an algorithm from JWA (RFC 7518): alg: RS256.
  • Verified with a key published as a JWK (RFC 7517), found in a JWKS served at https://login.example.com/.well-known/jwks.json.
  • Identified by the header’s kid: 2ae, which matches the kid of the right JWK inside that JWKS.

One sentence ties it all together: an OIDC access token is a JWT, serialized as a compact JWS, signed with an algorithm from JWA, whose public key is published as a JWK inside a JWKS, identified by the kid in the header. Six acronyms, one single token.

Decoding is not decrypting

This is the misunderstanding that causes the most damage in production. “I decoded the JWT” and “I decrypted the JWT” are statements about completely different operations. In a JWS — again, the overwhelmingly common case — the payload is just base64url. Decoding it requires no key, no secret, no permission. It’s a public, reversible transformation that anyone holding the token does with one click. There’s nothing cryptographic about it.

The practical consequence is direct: confidentiality only comes from JWE. If you need the token’s content to be secret and you’re using a JWS, you have no confidentiality at all — you have a plaintext JSON object with an authenticity stamp on it. Putting a password, an API secret, or sensitive data in the payload of a JWS is the equivalent of writing them on a postcard and trusting that nobody will turn it over to read.

The signature protects against tampering, not against reading. It guarantees that an attacker can’t change role: user to role: admin without invalidating the token — provided the verification is done correctly. And “provided it’s done correctly” is exactly where an entire family of real vulnerabilities lives, from alg: none to algorithm confusion. If you want to understand how these checks fail and how attackers turn a poorly validated JWS into an authentication bypass, read the offensive companion to this article: JWT vulnerabilities.

Which one should you use?

The decision is rarely hard once you separate the guarantees:

  • Need integrity and authenticity? Use JWS. This is the case for practically every session token, access token, and ID token. You want to prove who issued it and prevent tampering — and you don’t care that the content is readable, because there’s no secret in it. Prefer asymmetric algorithms (RS256, ES256, PS256) when the issuer and verifier are distinct parties, so you don’t share a secret.
  • Need confidentiality? Use JWE. The content must be unreadable to third parties — and even to the client that stores the token. Accept the extra complexity and follow RFC 8725.
  • Need both? Use a nested JWT: sign first (producing a JWS) and then encrypt that JWS inside a JWE. Order matters — sign then encrypt is the recommended pattern, because it ensures the signature covers the original content and isn’t left exposed in the clear. The result is a JWE whose payload, once decrypted, reveals a JWS to verify.

And most important: when not to use JWT. For user sessions in traditional web applications, an opaque session cookie with server-side state is usually simpler and more secure — you can revoke instantly, you don’t carry sensitive data on the client, and you don’t depend on correct signature validation in every service. JWT shines in stateless, distributed, cross-service scenarios, where decentralized verification is worth the loss of immediate revocation. If you don’t have that problem, you may not need that solution. The right question is never “which algorithm?” but “do I really need a self-contained token?”.


The alphabet soup stops being scary the moment you swap the question “what does each acronym mean?” for “what guarantee does each piece provide?”. JOSE is the umbrella; JWT is what you’re saying; JWS proves it was you who said it; JWE hides what was said; JWA is the list of how; JWK is the key that unlocks the proof. Hold on to the one sentence that separates those who understand from those who repeat the myth: signing is not encrypting, and decoding was never decrypting.