Most developers use JSON Web Tokens every day. Far fewer can tell you the difference between a JWT, a JWS, and a JWE without hand-waving — and the reason isn't that the topic is hard. It's that the three words look like they belong to the same list, so we assume they compete with each other. They don't. Once you see what actually defines each one, the whole thing snaps into place.

Let's find out by starting with a quiz.

Four tokens. What is each one?

Here are four things you might receive over the wire. Before reading on, try to label each: is it a JWT? a JWS? a JWE? more than one at once? Keep your guesses — we'll come back to them at the end.

Token A — a typical login token from your auth server:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0

.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Token B — a signature attached to a firmware image:

{ "alg": "RS256" } . <the firmware's bytes> . <RSA signature>

Token C — something with five dot-separated parts:

eyJhbGci... . g_hEwksO... . 48V1_ALb... . 5eym8TW_... . XFBoMYUZ...

Token D — a token whose header says { "alg": "none" }, ending in a bare dot:

eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.

Hold that thought. To answer confidently, you need one idea most explanations skip over.

The idea that unlocks everything

JWS and JWE are defined by their structure. JWT is defined by its content.

They answer completely different questions:

  • JWS answers: how is this protected? → It's signed.

  • JWE answers: how is this protected? → It's encrypted.

  • JWT answers: what's inside this? → A JSON set of claims.

That's the whole trick. JWS and JWE describe the wrapper. JWT describes the filling. They're not three items on one menu — they're two different menus, and a normal token orders from both.

Now let's take each one on its own terms.

JWS — "this thing is signed"

A JWS (JSON Web Signature, RFC 7515) is any object of the shape:

header . payload . signature

The signature is computed over the header and payload with a key, so anyone who has the matching key can prove two things: the content is authentic (you know who produced it) and intact (nobody changed a byte). If they tamper with one character, the signature stops matching and verification fails.

Crucially, JWS does not care what the payload is. It can be a JSON object, an XML document, a firmware image, or arbitrary bytes. JWS only cares that the thing is signed. That's why a signed firmware blob is a perfectly valid JWS even though nobody would call it a token.

One thing a signature does not do: hide your data. A JWS payload is Base64-URL encoded, not encrypted — anyone can read it. Signing proves the data is real; it doesn't make it secret.

There's also a variation worth knowing: a detached JWS, where you remove the payload and leave the slot empty (header..signature). The payload travels separately — in an HTTP body, a file on disk — and the verifier plugs it back in to check the signature. Handy when the payload is huge or already lives in its own channel, so you don't want to duplicate it inside the token.

JWE — "this thing is encrypted"

A JWE (JSON Web Encryption) is the confidential cousin. Where a JWS leaves the payload readable, a JWE turns it opaque — a string of characters nobody can read without the right key.

Because encryption needs more moving parts, a JWE has five components instead of three:

header . encrypted key . iv . ciphertext . auth tag

  • Header — which algorithms are in play.

  • Encrypted key — the key that scrambled the payload, itself encrypted for the recipient.

  • IV — random data so identical inputs don't produce identical output.

  • Ciphertext — the actual encrypted payload.

  • Auth tag — proves the ciphertext wasn't messed with.

The data flow is the mirror image of signing. With JWS you sign with a private key so everyone with the public key can verify. With JWE, anyone with the public key can encrypt, but only the holder of the private key can decrypt and read.

JWT — "the contents are a claims set"

A JWT (JSON Web Token, RFC 7519) is a JSON set of claims — assertions about a subject. You've seen them:

{ "iss": "auth.example.com", "sub": "1234567890", "role": "admin", "exp": 1735689600 }

Those short keys are registered claims: iss (issuer), sub (subject/user), aud (audience), exp (expiry), and friends. A JWT is defined entirely by this content — a claims set — and says nothing, on its own, about how it's protected.

So how is it protected? Here's the part that trips everyone up:

A JWT is a claims set wrapped as either a JWS (signed) or a JWE (encrypted). Always one of the two — there's no third option.

The JWT doesn't have its own wire format. It borrows the shape of whichever container you pick. Choose JWS and it comes out as three dotted parts. Choose JWE and it comes out as five. The claims are the filling; the container decides the shape.

That single sentence dissolves the most common misconception, which we'll hit head-on in a moment.

Back to the quiz

Now the four tokens answer themselves. The move is always the same: check the structure to find the container, then check the content to see if it's a JWT.

Token A — the login token → JWT and JWS.
Three parts, so it's signed → a JWS. Decode the middle and you find sub, role, exp — a claims set → also a JWT. This is the everyday case, and it's exactly why people think "JWT" and "JWS" mean the same thing. They're seeing both at once.

Token B — the firmware signature → JWS, but not a JWT.
Same header.payload.signature shape, so it's a JWS. But the payload is firmware bytes, not a JSON claims set — so it is not a JWT. Signed, yes; a token, no.

Token C — the five-part token → JWT and JWE, but not JWS.
Five parts means it's encrypted → a JWE. The claims still exist — they're sitting inside that encrypted ciphertext slot, unreadable until you decrypt — so it's still a JWT (because the claims are in JSON format - which you can’t see because it’s encrypted). It is not a JWS, because it was never put into the three-part signed form.

Token D — alg: none → JWT and JWS (an Unsecured JWS).
The payload is a claims set → a JWT. And despite having no real signature, it's still a JWS. Look at that trailing dot: the signature slot is present; it just holds the empty string. none isn't the absence of an algorithm — it's a registered alg value, and the spec calls the result an Unsecured JWS, formatted identically to any other JWS. RFC 7519 defines an Unsecured JWT precisely this way: a JWS with alg: none and an empty signature.

This is the article's own thesis biting back in a useful way — JWS is defined by structure, not by whether it's actually protected. An alg: none token is a catastrophically bad JWS, but structurally it's a JWS. And it's dangerous exactly because of that: it looks like a normal token, but anyone can rewrite the payload freely (the classic "alg: none attack"). Almost never use it.

Note the symmetry with detached JWS, too — both empty out a slot without leaving the container:

detached JWS: header . . signature ← payload slot empty

unsecured JWS: header . payload . ← signature slot empty

Here's the whole picture as a grid. The row picks the container; the column decides whether it's a JWT:

payload = JSON claims

payload = anything else

signed → JWS

Token A → JWT + JWS

Token B → JWS only

alg: noneUnsecured JWS

Token D → JWT + JWS

an unsecured JWS of arbitrary data

encrypted → JWE

Token C → JWT + JWE

e.g. an encrypted JWK Set → JWE only

Read it that way and the rivalry disappears. JWT and JWS were never competing — one is chosen by the column, the other by the row. Your normal login token is simply the top-left cell: both at the same time.

Two things this grid makes precise:

  • Every JWT is a JWS or a JWE. No exceptions — that's why alg: none sits in the JWS rows, not off in a third category. RFC 7519 puts it plainly: a JWT is a claims set encoded in a JWS and/or JWE structure. ("And/or" covers nested JWTs — sign the claims into a JWS, then encrypt that JWS inside a JWE, and you get both at once.)

  • JSON alone doesn't make a JWT — it has to be claims. That bottom-right cell is real: encrypt a private JWK Set and you have a JWE whose payload is JSON, yet it holds keys, not claims (iss, sub, exp), so it's not a JWT. RFC 7516's own examples encrypt a plain English sentence — the spec's canonical JWE isn't a JWT either. In practice this corner is sparse, since people reaching for JWE are almost always encrypting claims, but it proves the point: JWE, like JWS, is a container defined by structure and doesn't care what you put in it.

The subtlety that causes half the confusion: noun vs verb

There's one more reason "JWS" feels slippery: the same three letters do two jobs.

  • JWS as a process/standard (the "verb-y" sense): the act and rules of signing. "We use JWS to protect this token." "JWS defines how the signature is computed." Here it names the mechanism — RFC 7515, the procedure of running header + payload through an algorithm with a key.

  • JWS as an object (the noun): the thing you end up with. "This string is a JWS." "Send me the JWS." Here it names the complete header.payload.signature object in your hand.

Nobody literally conjugates "to JWS," so the verb is informal shorthand — but the two senses are real, and mixing them up is where people go wrong. The spec itself keeps them separate: it talks about the JWS Signing Input (the process side) versus a JWS (the object side), and it gives precise names to the pieces — JWS Payload, JWS Protected Header, and JWS Signature.

That last one matters: notice the standard calls the third chunk the JWS Signature — a part of the JWS, not the whole thing. Which corrects the single most common mistake:

"JWS is the signature part."
JWS is the entire signed object — header.payload.signature. The signature is just one component inside it.

And the mirror-image mistake, the one that makes Token C feel wrong at first:

"A JWT must be header.payload.signature."

It mustn't. Three dotted parts is the JWS shape, not the JWT shape. A JWT only looks like that when its container is a JWS. Encrypt it instead, and it wears the five-part JWE shape (that's Token C). Counting the dots doesn't tell you "is this a JWT?" — it tells you "which container is this JWT riding in?"

"A JWT is a claims set. It wears the shape of its container — three parts if signed (JWS), five parts if encrypted (JWE)."

The one-paragraph version

A JWS is anything in the signed structure (header.payload.signature); it doesn't care what the payload is. A JWE is anything in the encrypted structure (five opaque parts). A JWT is a JSON claims set — and it isn't a third format at all; it's content that always rides inside either a JWS or a JWE. So a signed login token is both a JWT and a JWS at once, an encrypted one is both a JWT and a JWE, a signed firmware blob is a JWS but not a JWT, an encrypted JWK Set is a JWE but not a JWT, and even an alg: none token is still a JWT in a JWS — just an Unsecured one with an empty signature. Check the structure for the container, check the content for the JWT — and the three words stop fighting for the same seat.

Thanks for Reading.
- Arian

Keep Reading