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.
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
What you’ll learn · Before you start
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
On this page
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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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: 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
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
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Interview questions
-
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.
-
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.
-
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.
-
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.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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: noneand 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
- RFC 7515: JSON Web Signature (JWS) (Internet Engineering Task Force (IETF))
- RFC 7516: JSON Web Encryption (JWE) (Internet Engineering Task Force (IETF))
- RFC 7517: JSON Web Key (JWK) (Internet Engineering Task Force (IETF))
- RFC 7518: JSON Web Algorithms (JWA) (Internet Engineering Task Force (IETF))
- RFC 7519: JSON Web Token (JWT) (Internet Engineering Task Force (IETF))
- RFC 8725: JSON Web Token Best Current Practices (Internet Engineering Task Force (IETF))
- RFC 8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE) (Internet Engineering Task Force (IETF))
- RFC 9864: Fully-Specified Algorithms for JOSE and COSE (Internet Engineering Task Force (IETF))
- RFC 9964: ML-DSA for JOSE and COSE (Internet Engineering Task Force (IETF))
- RFC 9901: Selective Disclosure for JSON Web Tokens (Internet Engineering Task Force (IETF))
- OWASP Application Security Verification Standard 5.0, V9 Self-contained Tokens (OWASP Foundation)
- Node.js 24 documentation: Web Crypto API (OpenJS Foundation (Node.js))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress