Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 11 – Tokens, OTPs and passkeys

JOSE internals: JWS, JWE and JWK

How a JWS is signed and verified against RFC 7515, what the two layers of a JWE do, and how to pin algorithms and keys so a token header cannot choose them.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 6 of 20 minutes free
Free: A JWS signs two Base64url strings joined by a dot · The verifier chooses the algorithm and the key, never the token. With a pass: 3 in-depth parts and 3 interview answers, about 14 minutes.

What you will learn

  • Build and verify a JWS from Web Crypto primitives and check it against RFC 7515
  • Explain the key-management and content-encryption layers of a JWE
  • Configure each key with its own algorithm and key type so that a token header cannot choose them
  • Use a JWKS, key IDs and current algorithm names such as Ed25519 when rotating keys

Before you start

RFC 7515’s first signed example is 179 characters long. The first 135 are a header and a payload that anyone can decode, and the last 43 are the signature that makes them worth believing. A verifier that lets the token’s own header pick the algorithm or the key has handed the decision to whoever wrote the token.

This lesson opens the format layer by layer. A JWS: JSON Web Signature: a signed message made of a header, a payload and a signature. In its compact form these are three Base64url strings joined by dots. signs, and a JWE: JSON Web Encryption: an encrypted message in which one layer protects a content key for the recipient and a second layer encrypts the data with it. The compact form has five parts. encrypts. A JWK: JSON Web Key: a cryptographic key written as a JSON object, with members such as the key type, a key ID and the key material. writes a key as JSON, and a JWKS publishes a set of them. You will rebuild the RFC’s example with Web Crypto, see why a verifier checks the exact bytes it received, and learn the rule that closes most of the format’s known holes: the verifier chooses the algorithm and the key, never the token.

A JWS signs two Base64url strings joined by a dot

A JWS in its compact form is three Base64url strings joined by dots: header, payload and signature. The header is a JSON object that says how the signature was made (alg) and often which key made it (kid). The payload is any bytes; in a JWT: JSON Web Token: a compact, URL-safe way to carry claims. The claims are a JSON object, usually the payload of a signed JWS, so anyone can read them and the signature shows whether they were changed. it is the JSON object of the claims. The signature covers the ASCII text of the first two strings with the dot between them, the signing input: The ASCII text a JWS signature covers: the Base64url header, a dot and the Base64url payload, exactly as they appear in the token., and nothing else. RFC 7515 Appendix A.1 signs a header of 30 bytes and a payload of 70 with HMAC SHA-256. Step through how its 179 characters come about:

Interactive · stage by stage How the JWS of RFC 7515 is put together Step through five stages. The numbers are those of the RFC's first example, signed with HMAC SHA-256.
How the JWS of RFC 7515 is put together, step 5 of 5. Showing Write the header, Write the payload, Encode both, Sign the text, Join with dots. In focus: Join with dots.Write the headeralg and typ, 30 bytesWrite the payloadthe claims, 70 bytesEncode bothBase64url, 40 and 94 charsSign the textan HMAC of 135 charactersJoin with dots179 characters in all

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The header. The header is a JSON object saying how the token is signed: here "alg":"HS256" and "typ":"JWT". The RFC's version has a line break and a space inside it, 30 bytes in all, and those exact bytes are what gets signed.
  2. The payload. The payload is any bytes. For a JWT it is the JSON of the claims: the issuer "joe", an expiry time and one claim of the RFC's own. It is 70 bytes, again with line breaks and spaces kept as written.
  3. Encode as Base64url. Each part becomes Base64url text without padding: 40 characters for the header and 94 for the payload. From here on the tokens are plain ASCII, safe in a header, a cookie or a URL.
  4. Sign the text. The signature covers the 135 ASCII characters of the two encoded parts with a dot between them. Nothing else is signed: not the decoded JSON, and not the signature itself. HMAC SHA-256 with the RFC's 64-byte key gives 32 bytes.
  5. Join with dots. The signature is encoded too, 43 characters, and the three parts are joined: 135 + 1 + 43 = 179 characters. The first 135 are readable by anyone; the last 43 are what makes them worth believing.

The program redoes the example with Web Crypto. It builds both parts from the RFC’s bytes, signs them with the RFC’s key, compares the result with the 43 characters printed in the RFC, and then tries the habit that most often breaks verification:

RFC 7515 Appendix A.1, rebuilt and checked with Web Crypto JavaScript · jws-a1-js/jws_a1.mjs
// RFC 7515 Appendix A.1: a JWS with HMAC SHA-256, built and checked with Web Crypto. A JWS signs the ASCII text of the
// Base64url header, a dot and the Base64url payload; the program shows that, then why a verifier must check the
// segments exactly as they arrived.
const subtle = globalThis.crypto.subtle;
const b64u = (bytes) => Buffer.from(bytes).toString('base64url');
const fromB64u = (text) => Buffer.from(text, 'base64url');
const text = (s) => new TextEncoder().encode(s);

// The header and payload of the RFC, byte for byte: each has a carriage return, a line feed and one space inside.
const header = '{"typ":"JWT",\r\n "alg":"HS256"}';
const payload = '{"iss":"joe",\r\n "exp":1300819380,\r\n "http://example.com/is_root":true}';
// The RFC's key as a JWK: "oct" means a raw secret, and "k" is its Base64url text.
const jwk = { kty: 'oct', k: 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow' };
const published = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; // the signature printed in the RFC

const key = await subtle.importKey('jwk', jwk, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
const signingInput = `${b64u(text(header))}.${b64u(text(payload))}`;
const signature = b64u(await subtle.sign('HMAC', key, text(signingInput)));

console.log(`header    ${text(header).length} bytes  ${b64u(text(header))}`);
console.log(`payload   ${text(payload).length} bytes  ${b64u(text(payload)).slice(0, 40)}…`);
console.log(`signed    ${signingInput.length} characters: the two segments and the dot between them`);
console.log(`HMAC      ${signature}`);
console.log(`RFC 7515  ${published}   same: ${signature === published}`);
console.log(`compact   ${signingInput.split('.').length + 1} parts, ${signingInput.length + 1 + signature.length} characters in all`);

// A verifier checks the signature over the segments as received. Parsing the header and writing it out again
// changes its bytes, so the signature no longer fits, although the JSON means the same.
const received = `${signingInput}.${signature}`;
const [h, p, s] = received.split('.');
const verifyOn = async (a, b) => subtle.verify('HMAC', key, fromB64u(s), text(`${a}.${b}`));
const rewritten = b64u(text(JSON.stringify(JSON.parse(fromB64u(h).toString()))));
console.log('');
console.log(`verify on the segments as received          ${await verifyOn(h, p)}`);
console.log(`header read as JSON and written out again   ${JSON.stringify(JSON.parse(fromB64u(h).toString()))}`);
console.log(`  same Base64url text as received: ${rewritten === h}, verify on it: ${await verifyOn(rewritten, p)}`);

Output

header    30 bytes  eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
payload   70 bytes  eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkz…
signed    135 characters: the two segments and the dot between them
HMAC      dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
RFC 7515  dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk   same: true
compact   3 parts, 179 characters in all

verify on the segments as received          true
header read as JSON and written out again   {"typ":"JWT","alg":"HS256"}
  same Base64url text as received: false, verify on it: false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node jws_a1.mjs

The signature is the RFC’s. Now the last three lines. The RFC’s header has a carriage return, a line feed and a space inside it, so the text that was signed is not the compact JSON a parser writes back. Reading the header as JSON and writing it out again gave a different Base64url string, and the signature check on it failed, although the JSON means the same thing. A verifier therefore checks the signature over the two strings exactly as they arrived, and reads their contents afterwards.

Checkpoint

Check: What a JWS signs

  1. Question 1 of 2 What does the signature of a compact JWS cover?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The ASCII text of the encoded header, a dot and the encoded payload

    The program printed "signed 135 characters: the two segments and the dot between them". The third part, 43 characters, is the signature itself.

  2. Question 2 of 2 A verifier reads the header of RFC 7515's example as JSON and writes it back as compact JSON before it checks the signature. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The check fails, because the header has other bytes now and so another Base64url text

    Verify the signature over the two parts exactly as they arrived, and read their contents afterwards.

The verifier chooses the algorithm and the key, never the token

The header’s alg says which algorithm the issuer used, and until the signature has been checked it is text that anyone could have written. RFC 8725, the best current practice for JWTs, therefore says that a verifier takes its set of algorithms from its own configuration and uses each key with exactly one algorithm, checking that the header agrees. When the header is allowed to choose, a token can pick the weakest check that will accept it. An alg of none means no signature at all. algorithm confusion: A flaw in which a verifier lets the token's header choose the algorithm, so a token made for the wrong kind of check is accepted, such as an HMAC keyed with an RSA public key. One algorithm per key prevents it. means an RSA public key, which everyone has, used as the key of an HMAC check: a verifier that follows the header accepts a token that anyone holding the public key could have built. ASVS 5.0 requirement 9.1.2 asks for the same rule in its own words, an allowlist of algorithms for each context that never includes none.

Four headers show the difference between a verifier that follows the header and one that pins one algorithm to each key:

The header says Follows the header Pins one algorithm to each key
alg: none, no signature Accepts: no check runs Refuses: no key is pinned to none
alg: HS256, MAC keyed with the RSA public key Accepts: the public key is the HMAC secret Refuses: that key is pinned to RS256
jku or jwk naming the sender’s own key Accepts: the sender’s key checks the sender’s token Ignores it, and finds the key by kid
alg: RS256, signed by the issuer’s key Accepts Accepts

Keys follow the same rule. RFC 7515 calls the kid header a hint about which key was used, so a verifier uses it only to pick among keys it already trusts. A header that points at a key, such as jku, x5u or jwk, is never followed to wherever the token says.

Two more pieces complete the picture. A JWE encrypts instead of signing: one layer protects a content key, and a second layer encrypts the data with it. A JWK writes a key as JSON, and a JWKS is a set of them that a service publishes so that others can find its public keys by kid.

Key idea

A token may say how it was signed, but the verifier decides what it accepts: algorithms and keys come from its own configuration, and each key is used with one algorithm.

Checkpoint

Check: The verifier chooses the algorithm and the key

  1. Question 1 of 2 A token's header says alg is none and the token has no signature. Which verifier accepts it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: One that takes the algorithm from the header

    RFC 8725 asks for the verifier's own set of algorithms. ASVS 9.1.2 adds that the set never includes none.

  2. Question 2 of 2 With how many algorithms may one key be used, according to RFC 8725?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Exactly one, and the verifier checks it every time

    A key that can be used with several algorithms is the opening for algorithm confusion, where an RSA public key is used as an HMAC key.

Verifying under a policy, with the failures reproduced Pro: opens with a Pro, Premium or Ultimate pass

A verifier is a short list of checks in a fixed order, each with the reason it exists. This part sets a verifier that reads its rules from its own configuration against one that trusts the header, on eleven tokens made for the purpose, and then hands you the verifier to write.

In this part:

  • The checks in the order a verifier runs them
  • Eleven tokens against a weak and a fixed verifier
  • Write the verifier yourself, with sample tests
  • Verify a JWS under a policy the token cannot change
    • Results of the sample tests

1 diagram · 1 runnable example (JavaScript) · 1 exercise with sample tests · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

JWE: two layers, five parts and what the tag covers Pro: opens with a Pro, Premium or Ultimate pass

A JWE protects data in two layers: a content key that is wrapped for the recipient, and the data encrypted under that key. This part takes the RFC’s published example apart with Web Crypto, builds a JWE of its own and lists the choices that go wrong.

In this part:

  • Five parts, and what each one holds
  • Opening the RFC’s example and building one of our own
  • Choosing alg and enc without the known mistakes

1 diagram · 1 runnable example (JavaScript) · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Keys in rotation: JWKS, kid and current algorithm names Pro: opens with a Pro, Premium or Ultimate pass

A key lives for a limited time, and a service that verifies tokens has to keep working while it changes. This part reads a JWK member by member, runs a rotation on a timeline with a cache that fetches the key set, and sorts out the algorithm names that RFC 9864 changed.

In this part:

  • What a JWK holds, and the member that is never published
  • Rotating without refusing a good token
  • Algorithm names today, from Ed25519 to ML-DSA
  • What to keep from the three parts

1 diagram · 1 runnable example (JavaScript) · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: signed bytes and the verifier’s rule

The session below asks about the first two ideas of the lesson: what a JWS signs, and who decides the algorithm. It starts with the one you know less and brings back what you miss:

Practice that adapts to you

Practise this lesson

6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 What are the three parts of a compact JWS, in order?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: Header, payload and signature, each as Base64url, joined by dots

    header.payload.signature: three Base64url strings and two dots, 179 characters in RFC 7515's first example.

  2. Question 2 of 6 A JWS header encodes to 40 characters, its payload to 94 and its signature to 43. How many characters does the compact token have?

    Type a number.

    A hint

    Two dots join the three parts.

    Show the answer: Question 2 of 6

    Answer: 179 characters

    40 + 1 + 94 + 1 + 43 = 179. The first 135 characters are the signing input, 40 + 1 + 94, and the last 43 are the signature.

  3. Question 3 of 6 A proxy parses each part of a JWT and writes it back as compact JSON, so the claims are unchanged. Signatures start to fail behind it. Why?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: The signature covers the exact text that was signed, and rewriting the JSON changes that text although not its meaning

    The fix is to pass tokens through untouched. A verifier that re-encodes before checking would fail on valid tokens too, which is why it checks the received segments.

  4. Question 4 of 6 Who decides which algorithms a verifier accepts?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: The verifier, from its own configuration

    The token may describe how it was made, but only the verifier says what it will accept.

  5. Question 5 of 6 Why does a verifier that follows the header accept a token with alg set to none?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: The token has chosen the check, and "none" is the check that always passes

    A verifier with its own list of algorithms refuses none, because none is not on the list.

  6. Question 6 of 6 A service verifies RS256 tokens with an RSA public key and also accepts HS256 "because the library supports it". What can go wrong?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: A token signed with an HMAC keyed by the public key's text passes, and anyone holding the public key can build one

    Pin the algorithm to the key: the RSA key is for RS256 and nothing else. ASVS 9.1.2 advises ideally only symmetric or only asymmetric algorithms in one context.

Interview questions

  1. Fresher to mid · Warm-up

    A colleague says a JWT is encrypted because it looks like random text. What is wrong with that, and what do the three parts hold?

    A JWT in its usual form is signed, not encrypted. The three parts are the header, the payload and the signature, each written as Base64url, an encoding that anyone can reverse. The header names the algorithm and often the key, the payload holds the claims as JSON, and the signature is a MAC or a digital signature over the first two parts. Decoding the first two parts needs no key, so a JWT must never carry anything that the holder or an eavesdropper must not read, such as a password, an ID number or an internal note. The signature only shows that the claims were not changed and who made them, provided the verifier checks it with a key and an algorithm it chose itself. If the contents must stay hidden, use a JWE, which encrypts, or keep the data on the server and send an opaque reference.

  2. Mid to senior

    A code review finds that your JWT middleware reads the algorithm from each token's header and uses one configured key for all of them. Describe how this lets forged tokens in, then fix it without breaking the tokens your clients hold today.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    Last night an identity provider rotated its signing key and your API refused every token for about twenty minutes. Reconstruct the likely causes on both sides, then set the caching and rotation rules that would have kept both working.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A partner wants to receive a customer's name, phone number and loan amount through the customer's browser. Compare a signed JWT, an encrypted JWT and an opaque reference token, and choose, with your reasons.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is a compact JWS made of, and what does its signature cover?

    Three Base64url strings joined by dots: header, payload and signature. The header says how the signature was made (alg) and often which key (kid); the payload is any bytes, a JWT's claims in a JWT. The signature covers the ASCII text of the first two strings with the dot between them, the signing input, and nothing else: in RFC 7515's 179-character example, 135 characters are signed and 43 are the signature.

  2. Why must a verifier check a JWS over the strings exactly as they arrived?

    RFC 7515's header has a carriage return, a line feed and a space inside it, so the signed text is not the compact JSON that a parser writes back. Reading the header as JSON and writing it out again gave a different Base64url string, and the check failed although the JSON means the same thing. Check the two strings as received, then read their contents.

  3. Who chooses the algorithm and the key when verifying a JWT?

    The verifier, never the token. RFC 8725 says to take the set of algorithms from your own configuration and use each key with exactly one algorithm, checking that the header agrees; ASVS 5.0 (9.1.2) asks for an allowlist per context that never includes none. Until the signature is checked, alg is text anyone could have written.

  4. What does a verifier that follows the token's header accept?

    alg: none, so no check runs; alg: HS256 with the MAC keyed by the RSA public key, which anyone holding that key could have built (algorithm confusion); and a jku or jwk header naming the sender's own key. A verifier that pins one algorithm to each key refuses the first two and ignores the third, finding the key by kid.

  5. How should a verifier use kid, jku, x5u and jwk headers?

    RFC 7515 calls kid a hint about which key was used, so use it only to pick among keys the verifier already trusts. A header that points at a key (jku, x5u or jwk) is never followed to wherever the token says.

  6. What are a JWE, a JWK and a JWKS?

    A JWE encrypts instead of signing: one layer protects a content key and a second layer encrypts the data with it. A JWK writes a key as JSON. A JWKS is a set of JWKs that a service publishes so that others can find its public keys by kid.

Key takeaways

  • A compact JWS is three Base64url parts, and its signature covers the first two with the dot between them: 135 of the 179 characters of RFC 7515’s example.
  • Verify the signature over the two strings exactly as they arrived and read their contents afterwards, because the RFC’s own header does not survive being read and written back.
  • The verifier, not the token, chooses the algorithm and the key, and each key is used with exactly one algorithm.
  • A header-trusting verifier accepts alg: none and an HMAC check keyed with a public key.

Terms in this lesson

algorithm confusion
A flaw in which a verifier lets the token's header choose the algorithm, so a token made for the wrong kind of check is accepted, such as an HMAC keyed with an RSA public key. One algorithm per key prevents it.
content encryption key
The key that encrypts the data of a message in a JWE or a hybrid scheme. It is drawn fresh for each message, and a separate step protects it for the recipient.
JWE
JSON Web Encryption: an encrypted message in which one layer protects a content key for the recipient and a second layer encrypts the data with it. The compact form has five parts.
JWK
JSON Web Key: a cryptographic key written as a JSON object, with members such as the key type, a key ID and the key material.
JWKS
JWK Set: a JSON object whose keys member lists JWKs. A service publishes its public keys this way so that others can find them by key ID.
JWS
JSON Web Signature: a signed message made of a header, a payload and a signature. In its compact form these are three Base64url strings joined by dots.
JWT
JSON Web Token: a compact, URL-safe way to carry claims. The claims are a JSON object, usually the payload of a signed JWS, so anyone can read them and the signature shows whether they were changed.
signing input
The ASCII text a JWS signature covers: the Base64url header, a dot and the Base64url payload, exactly as they appear in the token.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.