{"id":479992,"date":"2026-05-17T05:02:58","date_gmt":"2026-05-17T05:02:58","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=479992"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=479992","title":{"rendered":"JWT: The Self-Contained Token"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h3>Why API Keys Aren\u2019t Always Enough<\/h3>\n<p>In Part II we saw that an API key is essentially a long, secret password your software shows to a server. It works, but it has a hidden cost: every time the key is used, the server must look it up in a database to find out what the key is allowed to do, whether it has expired, and whether it has been switched off. A\u00a0<strong>JSON Web Token (JWT)<\/strong>\u00a0removes that lookup by carrying all of that information\u00a0<em>inside the token itself<\/em>. This article explains the problem JWT solves and shows where it sits in the larger story of web authentication.<\/p>\n<p>Part I covered Basic Authentication \u2014 sending a username and password with every request. Part II covered API keys \u2014 replacing that reusable password with a single opaque secret string that identifies an application rather than a person.<\/p>\n<p>Both approaches share a quiet assumption:\u00a0<strong>the server already knows things about the credential, and it has to go and remember them.<\/strong><\/p>\n<p>Think of an API key as a numbered coat-check ticket. The ticket itself tells you nothing \u2014 it\u2019s just a number. To find out what the ticket entitles you to, the attendant has to walk into the back room, find the matching record, and read it. The ticket is meaningless without the back room.<\/p>\n<p>That \u201cback room\u201d is a database, and consulting it is called a\u00a0<strong>database roundtrip<\/strong>\u00a0\u2014 the server pauses, sends a question to the database, and waits for an answer before it can continue.<\/p>\n<h4>The hidden cost of a roundtrip<\/h4>\n<p>For most applications, one database lookup per request is perfectly fine. But consider what the server actually has to check each time an API key arrives:<\/p>\n<ul>\n<li>\n<p><strong>Permissions<\/strong>\u00a0\u2014 what is this key allowed to do?<\/p>\n<\/li>\n<li>\n<p><strong>Expiry<\/strong>\u00a0\u2014 is this key still valid, or has it passed its end date?<\/p>\n<\/li>\n<li>\n<p><strong>Status<\/strong>\u00a0\u2014 has someone revoked or suspended this key?<\/p>\n<\/li>\n<\/ul>\n<p>All three answers live in the database, not in the key. So\u00a0<em>every single request<\/em>\u00a0triggers a roundtrip before any real work happens.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/4fa\/592\/113\/4fa5921136e59299566490560f942085.png\" alt=\"Figure 1. API Keys and Database Roundtrips\" width=\"1651\" height=\"790\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/4fa\/592\/113\/4fa5921136e59299566490560f942085.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/4fa\/592\/113\/4fa5921136e59299566490560f942085.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 1. API Keys and Database Roundtrips<\/figcaption><\/div>\n<\/figure>\n<p>This becomes a problem at scale in two specific situations:<\/p>\n<ol>\n<li>\n<p><strong>High request volume.<\/strong>\u00a0A popular API might handle thousands of requests per second. Thousands of identity lookups per second is real, measurable load \u2014 and load that does nothing for the user except verify who they are.<\/p>\n<\/li>\n<li>\n<p><strong>Distributed systems.<\/strong>\u00a0Modern applications are often split into many small, independent services (commonly called\u00a0<strong>microservices<\/strong>\u00a0\u2014 a single application built as a collection of small, separately running programs that talk to each other). If a request passes through five services, and each one independently re-checks the caller\u2019s identity against the database, that\u2019s five roundtrips for one user action. The database becomes a bottleneck that every service depends on.<\/p>\n<\/li>\n<\/ol>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/0bd\/ff3\/bb9\/0bdff3bb9748308291f73650807ff21c.png\" alt=\"Figure 2. API Key Validation Scaling Problem\" width=\"1481\" height=\"839\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/0bd\/ff3\/bb9\/0bdff3bb9748308291f73650807ff21c.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/0bd\/ff3\/bb9\/0bdff3bb9748308291f73650807ff21c.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 2. API Key Validation Scaling Problem<\/figcaption><\/div>\n<\/figure>\n<p>The deeper issue is\u00a0<strong>state<\/strong>. A server that must consult a database to understand a credential is a\u00a0<strong>stateful<\/strong>\u00a0server \u2014 it cannot make a decision on its own. It always needs the back room.<\/p>\n<h4>The core idea behind JWT<\/h4>\n<p>JWT flips the model. Instead of handing the server a meaningless ticket and forcing it to look up the details, JWT hands the server a ticket that\u00a0<em>has the details printed on it<\/em>\u00a0\u2014 and stamped in a way that proves the printing wasn\u2019t forged.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/727\/3e7\/391\/7273e739146d833cc9ce22f1aaf451a5.png\" alt=\"Figure 3. JWT and Stateless Validation\" width=\"1232\" height=\"702\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/727\/3e7\/391\/7273e739146d833cc9ce22f1aaf451a5.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/727\/3e7\/391\/7273e739146d833cc9ce22f1aaf451a5.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 3. JWT and Stateless Validation<\/figcaption><\/div>\n<\/figure>\n<p>To stay with the analogy: a JWT is less like a coat-check number and more like a\u00a0<strong>boarding pass<\/strong>. A boarding pass already states your name, your flight, your seat, and your boarding time, right there on the paper. The gate agent doesn\u2019t phone headquarters to look you up \u2014 they read the pass and check that it\u2019s genuine. The information travels\u00a0<em>with<\/em>\u00a0the traveler.<\/p>\n<p>This property has a precise name. A server that can validate a token using only the token itself (plus a verification key it already holds) is\u00a0<strong>stateless<\/strong>\u00a0\u2014 it holds no per-request memory and needs no back room. We will unpack exactly\u00a0<em>how<\/em>\u00a0a JWT proves it hasn\u2019t been forged later, when we open up its three-part structure. For now, the one idea to carry forward is the trade:<\/p>\n<blockquote>\n<p>An API key keeps the metadata in the database and gives you a pointer to it. A JWT puts the metadata in the token and gives you a way to trust it.<\/p>\n<\/blockquote>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/942\/e2d\/616\/942e2d616c7a501af9cff74a47a79493.png\" alt=\"Figure 4. API Keys vs. JWT comparison\" width=\"1348\" height=\"678\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/942\/e2d\/616\/942e2d616c7a501af9cff74a47a79493.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/942\/e2d\/616\/942e2d616c7a501af9cff74a47a79493.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 4. API Keys vs. JWT comparison<\/figcaption><\/div>\n<\/figure>\n<p>That trade is powerful, but \u2014 as Part III will explore honestly \u2014 it is not free. Information printed on a token cannot be un-printed, which creates a genuine difficulty when you need to cancel a token early.<\/p>\n<h3>A Few Words of History<\/h3>\n<p>Every technology arrives at a particular moment for a particular reason. JWT is no exception \u2014 it appeared just as the web was shifting away from a model that had quietly dominated for over a decade.<\/p>\n<h4>The world before: the session cookie<\/h4>\n<p>For most of the 2000s, web authentication worked through\u00a0<strong>server-side sessions<\/strong>. When you logged in, the server created a record of your session in its own memory or database and handed your browser a small identifier \u2014 a\u00a0<strong>session cookie<\/strong>. On every later request, your browser sent that cookie back, and the server looked it up to remember who you were.<\/p>\n<p>This is the same coat-check pattern from the previous session above: the cookie is a meaningless number, and the server holds all the real information. It worked well when a website was a single server. But it tied every logged-in user to a specific machine\u2019s memory. Spread your traffic across many servers, and you hit a problem \u2014 a user logged in on <strong>Server A<\/strong> is a stranger to <strong>Server B<\/strong>, because <strong>Server B<\/strong> never created that session record.<\/p>\n<h4>The pressure that created JWT<\/h4>\n<p>Two trends in the early 2010s made server-side sessions increasingly awkward.<\/p>\n<p>First, applications stopped being single servers. They were spread across fleets of machines, and increasingly split into\u00a0<strong>microservices<\/strong>\u00a0\u2014 many small programs, each handling one job. A shared, central session store became a bottleneck that every service had to consult.<\/p>\n<p>Second, the\u00a0<em>clients<\/em>\u00a0changed. The web was no longer just browsers loading full pages. It was single-page applications (SPAs) \u2014 websites that load once and then behave like desktop apps \u2014 and native mobile apps, often talking to APIs hosted on entirely different domains. Cookies, which are tightly bound to a single domain by design, fit this new world poorly.<\/p>\n<p>The industry needed a credential that any server could verify on its own, without a shared session store, and that wasn\u2019t anchored to one domain. The answer was to make the token\u00a0<em>carry its own proof of identity<\/em>.<\/p>\n<h4>RFC 7519: the standard<\/h4>\n<p>The work happened inside the\u00a0<strong>IETF<\/strong>\u00a0(the Internet Engineering Task Force, the body that standardizes the protocols underlying the internet) as part of its OAuth working group \u2014 the same group designing the next generation of authorization standards. They needed a compact, secure token format to pass identity information around, and JWT was built to fill that gap.<\/p>\n<p>The result was published in\u00a0<strong>May 2015 as <\/strong><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7519\" rel=\"noopener noreferrer nofollow\"><strong>RFC 7519<\/strong><\/a>\u00a0\u2014 the formal specification that defines what a JSON Web Token is. An\u00a0<strong>RFC<\/strong>\u00a0(\u201cRequest for Comments\u201d) is the document format the IETF uses for internet standards; despite the tentative-sounding name, a published RFC is the authoritative definition.<\/p>\n<p>A point worth noting: JWT was never a lone invention. It is the centerpiece of a small family of related standards \u2014 collectively called\u00a0<strong>JOSE<\/strong>\u00a0(JavaScript Object Signing and Encryption) \u2014 that also define how to sign tokens (JWS), encrypt them (JWE), and represent cryptographic keys (JWK).<\/p>\n<p>For most practical purposes, though, \u201cJWT\u201d is the term everyone uses, and it\u2019s the one we\u2019ll use here.<\/p>\n<h4>From standard to ubiquity<\/h4>\n<p>A specification only matters if people adopt it, and JWT\u2019s adoption was unusually fast. The identity company\u00a0<a href=\"https:\/\/auth0.com\" rel=\"noopener noreferrer nofollow\"><strong>Auth0<\/strong><\/a>, founded in 2013, built much of its product and developer education around JWT and promoted the format heavily \u2014 including\u00a0<a href=\"http:\/\/jwt.io\" rel=\"noopener noreferrer nofollow\"><code>jwt.io<\/code><\/a>, a free online debugger that let developers paste in a token and see its decoded contents instantly. For a great many engineers, that tool was their first hands-on encounter with the format.<\/p>\n<p>From there, JWT spread through the ecosystem. It became the default token format for\u00a0<a href=\"https:\/\/openid.net\" rel=\"noopener noreferrer nofollow\"><strong>OpenID Connect<\/strong><\/a>\u00a0\u2014 the identity layer built on top of <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" rel=\"noopener noreferrer nofollow\">OAuth 2.0<\/a>, and the subject we\u2019ll reach later in this series. Cloud platforms adopted it for service-to-service authentication. Web frameworks in nearly every programming language shipped libraries to create and verify it. Within a few years of RFC 7519, JWT had moved from a new proposal to a default assumption.<\/p>\n<h4>Why the history matters<\/h4>\n<p>This background isn\u2019t trivia \u2014 it explains the\u00a0<em>shape<\/em>\u00a0of the technology you\u2019re about to take apart. JWT looks the way it does because it was designed for a specific world: distributed systems with no shared memory, and clients scattered across domains and devices.<\/p>\n<p>That origin also explains its central trade-off. JWT was built to let a server make a decision\u00a0<em>on its own<\/em>, without calling back to a central store. That independence is exactly the feature that makes JWT fast and scalable \u2014 and, as we\u2019ll see, exactly the feature that makes a token hard to cancel once it\u2019s been issued. The strength and the weakness are the same design decision, viewed from two sides.<\/p>\n<p>With that context in place, we can now open up an actual token and see what those three dot-separated parts really contain.<\/p>\n<h3>The Anatomy of a JWT<\/h3>\n<p>This is the heart of subject. Once you can see what a JWT actually\u00a0<strong><em>is<\/em><\/strong>, every later topic \u2014 claims, signatures, validation, security \u2014 becomes far easier to follow. So we\u2019ll take a real token apart, piece by piece.<\/p>\n<h4>Three parts, two dots<\/h4>\n<p>A JWT, when written out, looks like a long, slightly intimidating string of gibberish:<\/p>\n<pre><code class=\"json\">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBZGEifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>It looks random. It isn\u2019t. Look closely and you\u2019ll spot\u00a0<strong>two dots<\/strong>\u00a0dividing it into\u00a0<strong>three parts<\/strong>:<\/p>\n<pre><code>header  .  payload  .  signature<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That structure never changes. Every JWT, everywhere, is exactly three parts separated by dots:<\/p>\n<ol>\n<li>\n<p>The\u00a0<strong>header<\/strong>\u00a0\u2014 describes the token itself.<\/p>\n<\/li>\n<li>\n<p>The\u00a0<strong>payload<\/strong>\u00a0\u2014 carries the actual information.<\/p>\n<\/li>\n<li>\n<p>The\u00a0<strong>signature<\/strong>\u00a0\u2014 proves the first two parts haven\u2019t been tampered with.<\/p>\n<\/li>\n<\/ol>\n<p>The boarding-pass analogy from the first section holds up neatly here. The header is the small print at the top that says what kind of document this is. The payload is the part you actually care about \u2014 your name, your flight, your seat. The signature is the official stamp that proves the pass is genuine and not something printed at home.<\/p>\n<h4>Why it looks like gibberish: Base64URL<\/h4>\n<p>The reason a JWT looks unreadable is not encryption. It is\u00a0<strong>encoding<\/strong>\u00a0\u2014 and the distinction matters.<\/p>\n<ul>\n<li>\n<p><strong>Encryption<\/strong>\u00a0scrambles data so that\u00a0<em>no one<\/em>\u00a0can read it without a secret key.<\/p>\n<\/li>\n<li>\n<p><strong>Encoding<\/strong>\u00a0simply rewrites data into a different alphabet so it can travel safely. Anyone can reverse it.<\/p>\n<\/li>\n<\/ul>\n<p>A JWT\u2019s header and payload are merely\u00a0<strong>encoded<\/strong>, not encrypted. They use a scheme called\u00a0<strong>Base64URL<\/strong>\u00a0\u2014 a way of representing data using only letters, digits, and a couple of symbols that are safe to put inside a web address (a URL).<\/p>\n<p>This leads to the single most important \u2014 and most misunderstood \u2014 fact about JWTs:<\/p>\n<blockquote>\n<p><strong>The contents of a JWT are not secret.<\/strong>\u00a0Anyone who holds the token can decode the header and payload and read everything inside. The signature stops people from\u00a0<em>changing<\/em>\u00a0a token; it does nothing to\u00a0<em>hide<\/em>\u00a0it.<\/p>\n<\/blockquote>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/30f\/fd8\/df3\/30ffd8df32b7dab29d2a42601c5b5381.png\" alt=\"Figure 5. JWT Decoding vs. Hiding Secrets\" width=\"1223\" height=\"698\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/30f\/fd8\/df3\/30ffd8df32b7dab29d2a42601c5b5381.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/30f\/fd8\/df3\/30ffd8df32b7dab29d2a42601c5b5381.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 5. JWT Decoding vs. Hiding Secrets<\/figcaption><\/div>\n<\/figure>\n<p>We\u2019ll return to the security consequences of this later. For now, just hold onto it: a JWT protects against forgery, not against reading. Never put a password, a credit-card number, or any other secret inside it.<\/p>\n<h4>Part 1: The header<\/h4>\n<p>Take the first segment of our example token and reverse the Base64URL encoding, and you get a small block of\u00a0<strong>JSON<\/strong>\u2014 JavaScript Object Notation, the simple, human-readable\u00a0<code>key: value<\/code>\u00a0format used everywhere on the modern web:<\/p>\n<pre><code class=\"json\">{  \"alg\": \"HS256\",  \"typ\": \"JWT\"}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The header is metadata\u00a0<em>about the token<\/em>. It answers two questions:<\/p>\n<ul>\n<li>\n<p><code><strong>alg<\/strong><\/code>\u00a0(\u201calgorithm\u201d) \u2014 how the signature was created. Here,\u00a0<code>HS256<\/code>. We\u2019ll examine the algorithm choices in detail in part of the article; for now, treat it as a label naming the method used to make the stamp.<\/p>\n<\/li>\n<li>\n<p><code><strong>typ<\/strong><\/code>\u00a0(\u201ctype\u201d) \u2014 what kind of object this is. For a JWT, this is simply\u00a0<code>\"JWT\"<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>The header is short, and it\u2019s mostly there so the receiving server knows\u00a0<em>how<\/em>\u00a0to check the signature later.<\/p>\n<h4>Part 2: The payload<\/h4>\n<p>Decode the second segment and you get another block of JSON \u2014 and this is the part that does the real work:<\/p>\n<pre><code class=\"json\">{  \"sub\": \"12345\",  \"name\": \"Ada\"}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The payload carries the\u00a0<strong>claims<\/strong>\u00a0\u2014 the individual statements the token is making. The word is well chosen: each entry is a\u00a0<em>claim<\/em>, an assertion that \u201cthis is true.\u201d Here the token claims two things: the subject (<code>sub<\/code>) of this token is user\u00a0<code>12345<\/code>, and that user\u2019s name is\u00a0<code>Ada<\/code>. The payload is where identity, permissions, expiry times, and your own custom data live.<\/p>\n<h4>Part 3: The signature<\/h4>\n<p>The third segment is the only part that is genuinely cryptographic \u2014 and it\u2019s the part that makes a JWT trustworthy.<\/p>\n<p>Here is the problem the signature solves. We just established that anyone holding a token can read the payload. But could they also\u00a0<em>change<\/em>\u00a0it \u2014 swap\u00a0<code>\"sub\": \"12345\"<\/code>\u00a0for\u00a0<code>\"sub\": \"99999\"<\/code>\u00a0and impersonate another user? The answer is <strong>\u201cYES\u201d<\/strong>. Does it stay hidden? And the most important answer is here &#8212; <strong>\u201cNO\u201d<\/strong>, as the signature is what makes that attack fail because it becomes easily recognizable and server knows how to react on it.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/931\/960\/782\/931960782a1725a29f0639a51a5e6ff2.png\" alt=\"Figure 6. JWT Forgery Protection - The Signature\" width=\"1221\" height=\"697\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/931\/960\/782\/931960782a1725a29f0639a51a5e6ff2.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/931\/960\/782\/931960782a1725a29f0639a51a5e6ff2.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 6. JWT Forgery Protection &#8212; The Signature<\/figcaption><\/div>\n<\/figure>\n<p>When the server first creates the token, it performs a calculation. It takes the encoded header and the encoded payload, joins them with a dot, and feeds that \u2014 together with a\u00a0<strong>secret key that only the server knows<\/strong>\u00a0\u2014 into a one-way mathematical function. For our\u00a0<code>HS256<\/code>\u00a0example, that function is\u00a0<strong>HMAC-SHA256<\/strong>. The output is the signature:<\/p>\n<pre><code>signature = HMAC-SHA256(    base64url(header) + \".\" + base64url(payload),    secret)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Two properties of this function are what make the whole scheme work:<\/p>\n<ol>\n<li>\n<p><strong>It depends on every input.<\/strong>\u00a0Change a single character of the header or payload, and the output is completely different.<\/p>\n<\/li>\n<li>\n<p><strong>It can\u2019t be reversed or faked without the secret.<\/strong>\u00a0Knowing the inputs but not the secret, there is no practical way to compute the correct signature.<\/p>\n<\/li>\n<\/ol>\n<p>So when an attacker edits the payload to say\u00a0<code>\"sub\": \"99999\"<\/code>, the signature attached to the token no longer matches the modified content. When the server recomputes the signature from the tampered payload and compares it to the one in the token, the two don\u2019t match \u2014 and the token is rejected. Because the attacker doesn\u2019t have the secret key, they can\u2019t produce a fresh, valid signature for their forged payload either.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f2d\/3e9\/121\/f2d3e912110addc11719335d3f17d378.png\" alt=\"Figure 7. JWT Signature- The Key to Integrity and Honesty\" width=\"1233\" height=\"699\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/f2d\/3e9\/121\/f2d3e912110addc11719335d3f17d378.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f2d\/3e9\/121\/f2d3e912110addc11719335d3f17d378.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 7. JWT Signature- The Key to Integrity and Honesty<\/figcaption><\/div>\n<\/figure>\n<p>And this is the core idea worth carrying out of this section:<\/p>\n<blockquote>\n<p>The signature doesn\u2019t keep a JWT\u00a0<em>private<\/em>. It keeps a JWT\u00a0<em>honest<\/em>. It guarantees that the header and payload are exactly what the issuing server wrote, untouched since.<\/p>\n<\/blockquote>\n<h4>Building one by hand<\/h4>\n<p>The best way to dispel the sense that this is magic is to build a token with no library at all \u2014 just the standard tools any programming language provides. Here it is in Python:<\/p>\n<pre><code class=\"python\">import base64, json, hmac, hashlibdef base64url_encode(data: bytes) -&gt; str:    # Standard Base64, then made URL-safe: trailing '=' padding removed.    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()def create_jwt(payload: dict, secret: str) -&gt; str:    # 1. The header: declare the algorithm and type.    header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}    # 2. Encode the header and payload as Base64URL.    h = base64url_encode(json.dumps(header).encode())    p = base64url_encode(json.dumps(payload).encode())    # 3. Sign \"header.payload\" with the secret using HMAC-SHA256.    sig = hmac.new(secret.encode(), f\"{h}.{p}\".encode(), hashlib.sha256).digest()    # 4. Join all three parts with dots.    return f\"{h}.{p}.{base64url_encode(sig)}\"token = create_jwt({\"sub\": \"12345\", \"name\": \"Ada\"}, secret=\"my-secret-key\")print(token)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That is the\u00a0<em>entire<\/em>\u00a0mechanism. There is nothing hidden. A JWT is two pieces of JSON, encoded for safe transport, plus a signature computed from them. The four numbered steps in the code are the whole story.<\/p>\n<h4>The production path: use a library<\/h4>\n<p>Building a token by hand is the right way to\u00a0<em>understand<\/em>\u00a0JWTs. It is the wrong way to\u00a0<em>use<\/em>\u00a0them in real software.<\/p>\n<p>In production, always reach for a well-maintained, audited library \u2014\u00a0<code>PyJWT<\/code>\u00a0or\u00a0<code>python-jose<\/code>\u00a0in Python, and direct equivalents in every other major language. The same task becomes a single call:<\/p>\n<pre><code class=\"python\">import jwt  # the PyJWT librarytoken = jwt.encode(    {\"sub\": \"12345\", \"name\": \"Ada\"},    \"my-secret-key\",    algorithm=\"HS256\")<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This isn\u2019t laziness \u2014 it\u2019s safety. The hand-written version above is correct for\u00a0<em>creating<\/em>\u00a0tokens, but\u00a0<em>verifying<\/em>\u00a0them safely is full of subtle traps. A naive verifier can be tricked into accepting forged tokens, expired tokens, or tokens it should never have trusted. Mature libraries have already absorbed those lessons; your own code, written from scratch, has not.<\/p>\n<p>So build one by hand once, to see that the magic is just arithmetic. Then never do it again in production.<\/p>\n<p>With the structure of a token clear, we can turn to its most important part in detail \u2014 the payload, and the claims it carries.<\/p>\n<h3>Claims: What a Token Actually Says<\/h3>\n<p>In previous section we opened up a JWT and found the payload \u2014 the middle segment carrying the token\u2019s real content. Each piece of information in that payload is called a\u00a0<strong>claim<\/strong>. This section is about what those claims are, which ones the standard defines for you, and which ones you create yourself.<\/p>\n<h4>A claim is just a statement<\/h4>\n<p>The terminology sounds formal, but the idea is plain. A\u00a0<strong>claim<\/strong>\u00a0is a single statement the token makes about its subject \u2014 one\u00a0<code>key: value<\/code>\u00a0pair in the payload\u2019s JSON. The token \u201cclaims\u201d these things are true, and the signature is what makes that claim trustworthy.<\/p>\n<p>A payload is simply a collection of these statements:<\/p>\n<pre><code class=\"json\">{  \"iss\": \"auth.myapp.com\",  \"sub\": \"12345\",  \"aud\": \"api.myapp.com\",  \"iat\": 1716120000,  \"exp\": 1716123600,  \"role\": \"editor\",  \"tenant_id\": \"acme-corp\"}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Some of those keys \u2014\u00a0<code>iss<\/code>,\u00a0<code>sub<\/code>,\u00a0<code>aud<\/code>,\u00a0<code>iat<\/code>,\u00a0<code>exp<\/code>\u00a0\u2014 are part of the JWT standard and mean the same thing everywhere. Others \u2014\u00a0<code>role<\/code>,\u00a0<code>tenant_id<\/code>\u00a0\u2014 are invented by the application. That split is the central idea of this section.<\/p>\n<h4>Registered claims: the standard vocabulary<\/h4>\n<p>RFC 7519 defines a small set of\u00a0<strong>registered claims<\/strong>. These are not required \u2014 a JWT is free to omit any of them \u2014 but if you\u00a0<em>do<\/em>\u00a0use them, you must use them with the meaning the standard assigns. They have short, three-letter names to keep tokens compact.<\/p>\n<p>There are seven worth knowing:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">Claim<\/p>\n<\/th>\n<th>\n<p align=\"left\">Name<\/p>\n<\/th>\n<th>\n<p align=\"left\">What it means<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>iss<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Issuer<\/p>\n<\/td>\n<td>\n<p align=\"left\">Who created and signed this token.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>sub<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Subject<\/p>\n<\/td>\n<td>\n<p align=\"left\">Who or what the token is about \u2014 typically the user ID.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>aud<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Audience<\/p>\n<\/td>\n<td>\n<p align=\"left\">Who the token is\u00a0<em>intended for<\/em>\u00a0\u2014 which service should accept it.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>exp<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Expiration Time<\/p>\n<\/td>\n<td>\n<p align=\"left\">The moment after which the token must be rejected.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>nbf<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Not Before<\/p>\n<\/td>\n<td>\n<p align=\"left\">The moment before which the token is not yet valid.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>iat<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Issued At<\/p>\n<\/td>\n<td>\n<p align=\"left\">The moment the token was created.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>jti<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">JWT ID<\/p>\n<\/td>\n<td>\n<p align=\"left\">A unique identifier for this specific token.<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>A few of these deserve a closer look, because they do more than they first appear to.<\/p>\n<p><code><strong>iss<\/strong><\/code><strong>\u00a0(Issuer)<\/strong>\u00a0identifies the authority that minted the token \u2014 usually an authentication server like\u00a0<a href=\"http:\/\/auth.myapp.com\" rel=\"noopener noreferrer nofollow\"><code>auth.myapp.com<\/code><\/a>. A receiving service checks this so it only trusts tokens from a source it recognizes, and ignores tokens minted by anyone else.<\/p>\n<p><code><strong>aud<\/strong><\/code><strong>\u00a0(Audience)<\/strong>\u00a0is the one most often misunderstood, and it matters for security. It names the intended\u00a0<em>recipient<\/em>. If your authentication server issues tokens for several different services, the\u00a0<code>aud<\/code>\u00a0claim lets each service confirm \u201cthis token was meant for\u00a0<em>me<\/em>.\u201d A token minted for the billing service should be refused by the email service, even though both trust the same issuer. Skipping this check is a real vulnerability.<\/p>\n<p><code><strong>jti<\/strong><\/code><strong>\u00a0(JWT ID)<\/strong>\u00a0is a unique serial number for one individual token. On its own it does little. Its importance shows up later: when you need to\u00a0<em>cancel<\/em>\u00a0a specific token before it expires, the\u00a0<code>jti<\/code>\u00a0is the handle you use to do it. That\u2019s the revocation problem, and\u00a0<code>jti<\/code>\u00a0is the thread that connects to it.<\/p>\n<h4>The temporal claims:\u00a0iat,\u00a0nbf, and\u00a0exp<\/h4>\n<p>Three of the registered claims \u2014\u00a0<code>iat<\/code>,\u00a0<code>nbf<\/code>, and\u00a0<code>exp<\/code>\u00a0\u2014 are about\u00a0<em>time<\/em>, and together they give a token a lifespan. They are the mechanism behind one of JWT\u2019s most important properties: a token that automatically stops working.<\/p>\n<p>Laid out on a timeline, they mark out a window of validity:<\/p>\n<pre><code>   iat            nbf              now                      exp    \u2502              \u2502                \u2502                        \u2502    \u25cf\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25cf\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u25cf\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u25cf\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba  time  issued       valid from      this moment              expires after                   \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 token is VALID here \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Figure 8. Timeline for JWT claims<\/p>\n<ul>\n<li>\n<p><code><strong>iat<\/strong><\/code><strong>\u00a0(Issued At)<\/strong>\u00a0is the token\u2019s birth timestamp \u2014 when it was created.<\/p>\n<\/li>\n<li>\n<p><code><strong>nbf<\/strong><\/code><strong>\u00a0(Not Before)<\/strong>\u00a0is the moment the token\u00a0<em>starts<\/em>\u00a0being valid. Usually this is the same as\u00a0<code>iat<\/code>, but it can be set in the future to issue a token now that only \u201cswitches on\u201d later.<\/p>\n<\/li>\n<li>\n<p><code><strong>exp<\/strong><\/code><strong>\u00a0(Expiration Time)<\/strong>\u00a0is the moment the token\u00a0<em>stops<\/em>\u00a0being valid. After this instant, every correct server must reject it, no matter what else the token says.<\/p>\n<\/li>\n<\/ul>\n<p>The\u00a0<code>exp<\/code>\u00a0claim is what gives a JWT its self-limiting nature. An API key, by contrast, tends to live until a human remembers to revoke it. A JWT carries its own deadline. Set\u00a0<code>exp<\/code>\u00a0to 15 minutes after\u00a0<code>iat<\/code>, and the token simply expires 15 minutes later \u2014 no database, no cleanup job, no human action. This is central to managing the risk of a stolen token.<\/p>\n<p>One technical detail: these timestamps are stored as\u00a0<strong>Unix time<\/strong>\u00a0\u2014 the number of seconds since midnight UTC on January 1, 1970. That\u2019s why\u00a0<code>exp<\/code>\u00a0appears as a large integer like\u00a0<code>1716123600<\/code>\u00a0rather than a human-readable date. It\u2019s a universal, timezone-free way to pin down an exact instant, and every JWT library converts it for you.<\/p>\n<h4>Custom claims: your own data<\/h4>\n<p>The registered claims cover identity and timing. They say nothing about what a user is\u00a0<em>allowed to do<\/em>, because that is specific to your application. For everything else, you add\u00a0<strong>custom claims<\/strong>\u00a0\u2014\u00a0<code>key: value<\/code>\u00a0pairs you define yourself, with whatever names and meanings you choose.<\/p>\n<p>Custom claims are where JWT delivers on the promise from the section above \u2014 moving metadata\u00a0<em>into the token<\/em>\u00a0so the server doesn\u2019t need a database lookup. Typical examples:<\/p>\n<ul>\n<li>\n<p><code><strong>role<\/strong><\/code>\u00a0or\u00a0<code><strong>permissions<\/strong><\/code>\u00a0\u2014 what the user is authorized to do (<code>\"editor\"<\/code>,\u00a0<code>\"admin\"<\/code>, or a list like\u00a0<code>[\"read:invoices\", \"write:invoices\"]<\/code>). The receiving service reads this straight from the token and decides what to allow \u2014 no roundtrip required.<\/p>\n<\/li>\n<li>\n<p><code><strong>tenant_id<\/strong><\/code>\u00a0\u2014 in applications that serve many separate organizations from one system, this identifies which organization the user belongs to.<\/p>\n<\/li>\n<li>\n<p><code><strong>email<\/strong><\/code>,\u00a0<code><strong>name<\/strong><\/code>\u00a0\u2014 basic profile fields, included to save a lookup when displaying the user\u2019s identity.<\/p>\n<\/li>\n<\/ul>\n<p>This is exactly the design that makes a server\u00a0<strong>stateless<\/strong>: the answer to \u201cwho is this, and what may they do?\u201d travels inside the request itself.<\/p>\n<p>Two cautions, both flowing directly from facts already established.<\/p>\n<p>First \u2014 and this bears repeating because it is the most common JWT mistake \u2014\u00a0<strong>the payload is not secret.<\/strong>\u00a0Anyone holding the token can decode and read it. So custom claims may contain a user\u2019s role; they must never contain anything sensitive \u2014 no passwords, no API secrets, no private personal data.<\/p>\n<p>Second,\u00a0<strong>keep the payload small.<\/strong>\u00a0A JWT travels with\u00a0<em>every single request<\/em>\u00a0to your API. Every claim you add makes every request slightly larger. Stick to what the receiving service genuinely needs to make a decision; resist the temptation to pack a user\u2019s entire profile into the token.<\/p>\n<h4>Avoiding name collisions<\/h4>\n<p>If your application invents a custom claim called\u00a0<code>role<\/code>, and some other system your token passes through also uses\u00a0<code>role<\/code>\u00a0to mean something different, the two meanings collide. The JWT standard\u2019s recommended fix is to\u00a0<strong>namespace<\/strong>\u00a0custom claims \u2014 prefix them with a URL you control, so the name is globally unique:<\/p>\n<pre><code class=\"json\">{  \"sub\": \"12345\",  \"https:\/\/myapp.com\/role\": \"editor\"}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The URL doesn\u2019t have to point anywhere real; it\u2019s used purely as a unique prefix. For a closed system where you control every service, plain names like\u00a0<code>role<\/code>\u00a0are common and perfectly workable. For tokens that travel between organizations, namespacing prevents a class of subtle, hard-to-diagnose bugs.<\/p>\n<h4>The payload, in summary<\/h4>\n<p>A JWT\u2019s payload is a set of claims \u2014 statements the token asserts and the signature makes trustworthy. Some claims are\u00a0<strong>registered<\/strong>\u00a0by the standard:\u00a0<code>iss<\/code>,\u00a0<code>sub<\/code>, and\u00a0<code>aud<\/code>\u00a0answer\u00a0<em>who<\/em>;\u00a0<code>iat<\/code>,\u00a0<code>nbf<\/code>, and\u00a0<code>exp<\/code>\u00a0answer\u00a0<em>when<\/em>;\u00a0<code>jti<\/code>\u00a0gives the token a unique name. The rest are\u00a0<strong>custom claims<\/strong>\u00a0you define, and they are how identity and permissions ride along inside the request, sparing the server a database lookup.<\/p>\n<p>We now know what a token\u00a0<em>says<\/em>. The next question is\u00a0<em>how<\/em>\u00a0it proves it \u2014 which means looking closely at the signature, and the different algorithms used to create it.<\/p>\n<h3>Signature Algorithms: HS256, RS256, and ES256<\/h3>\n<p>Above we established what the signature\u00a0<em>does<\/em>: it makes a token honest, guaranteeing the header and payload haven\u2019t been altered since the issuer wrote them. We used\u00a0<code>HS256<\/code>\u00a0as the example and treated it as a black box. Now we open the box \u2014 because the choice of signing algorithm is one of the most consequential decisions you\u2019ll make when building a real system, and it turns entirely on a single question:\u00a0<strong>who is allowed to verify the token?<\/strong><\/p>\n<h4>Two families: symmetric and asymmetric<\/h4>\n<p>Every JWT signing algorithm belongs to one of two families. The difference between them is the difference between a shared password and a wax seal.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1e7\/79e\/11e\/1e779e11e7ea3fc2409f239a0152ef3a.png\" alt=\"Figure 9. JWT Signing - Symmetric vs. Asymmetric\" width=\"1224\" height=\"694\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/1e7\/79e\/11e\/1e779e11e7ea3fc2409f239a0152ef3a.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1e7\/79e\/11e\/1e779e11e7ea3fc2409f239a0152ef3a.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 9. JWT Signing &#8212; Symmetric vs. Asymmetric<\/figcaption><\/div>\n<\/figure>\n<p>A\u00a0<strong>symmetric<\/strong>\u00a0algorithm uses\u00a0<strong>one secret key for both jobs<\/strong>\u00a0\u2014 creating the signature and checking it. The same key signs and verifies. It\u2019s like a password shared between two parties: whoever knows it can both lock and unlock.<\/p>\n<p>An\u00a0<strong>asymmetric<\/strong>\u00a0algorithm uses\u00a0<strong>a pair of mathematically linked keys<\/strong>: a\u00a0<strong>private key<\/strong>\u00a0and a\u00a0<strong>public key<\/strong>. The private key creates signatures and is guarded closely. The public key only\u00a0<em>verifies<\/em>\u00a0signatures and can be shared freely \u2014 handed to anyone, posted publicly \u2014 without weakening anything. It\u2019s like a wax seal: the issuer owns the unique stamp (the private key), but anyone with a picture of the seal (the public key) can recognize a genuine one. Holding the public key lets you\u00a0<em>check<\/em>\u00a0a seal but never\u00a0<em>forge<\/em>\u00a0one.<\/p>\n<p>That single distinction \u2014 one shared key versus a public\/private pair \u2014 drives everything that follows.<\/p>\n<h4>HS256: the symmetric option<\/h4>\n<p><strong>HS256<\/strong>\u00a0stands for\u00a0<strong>HMAC using SHA-256<\/strong>. It is the symmetric choice, and it\u2019s the algorithm in every example so far.<\/p>\n<p>With HS256, one secret string both signs and verifies tokens. The server that issues a token uses the secret to compute the signature; the server that receives a token uses\u00a0<em>the same secret<\/em>\u00a0to recompute the signature and compare. If they match, the token is genuine.<\/p>\n<p>This is simple, fast, and produces compact signatures. But it carries one unavoidable consequence:\u00a0<strong>every party that needs to verify a token must hold the secret key.<\/strong>\u00a0And here is the catch \u2014 the verifying key and the signing key are the\u00a0<em>same key<\/em>. Any service that can\u00a0<em>check<\/em>\u00a0a token can therefore also\u00a0<em>mint<\/em>\u00a0a brand-new token that everyone else will trust.<\/p>\n<p>In a single, self-contained application, that\u2019s fine. The same server issues tokens and checks them. There\u2019s one secret, in one place, and no one else needs it.<\/p>\n<p>The trouble begins when the system grows.<\/p>\n<h4>RS256: the asymmetric option<\/h4>\n<p><strong>RS256<\/strong>\u00a0stands for\u00a0<strong>RSA Signature using SHA-256<\/strong>. RSA is a long-established public-key cryptosystem; what matters here is that RS256 is\u00a0<strong>asymmetric<\/strong>\u00a0\u2014 it uses the private\/public key pair.<\/p>\n<p>With RS256, the authentication server holds a\u00a0<strong>private key<\/strong>\u00a0and uses it \u2014 and only it \u2014 to sign tokens. It then publishes the matching\u00a0<strong>public key<\/strong>\u00a0for the world to see. Any other service can take that public key and verify a token\u2019s signature. But the public key\u00a0<em>cannot sign anything<\/em>. A service holding only the public key can confirm a token is genuine, yet has no power to forge one.<\/p>\n<p>That asymmetry solves the exact problem HS256 created.<\/p>\n<h4>The \u201caha\u201d: why this matters for microservices<\/h4>\n<p>Recall the microservices picture from Figure 1 \u2014 an application split into many small, independent services. Suppose one of them, the authentication service, issues tokens, and a dozen others need to verify them.<\/p>\n<p><strong>With HS256<\/strong>, every one of those dozen services must hold the shared secret. That\u2019s a real problem on two fronts:<\/p>\n<ul>\n<li>\n<p><strong>A wider attack surface.<\/strong>\u00a0The secret now lives in twelve places instead of one. A breach of\u00a0<em>any single service<\/em>leaks the key \u2014 and with it, the power to forge tokens that all twelve will trust.<\/p>\n<\/li>\n<li>\n<p><strong>Misplaced trust.<\/strong>\u00a0Every service that can\u00a0<em>verify<\/em>\u00a0can also\u00a0<em>issue<\/em>. A minor, low-privilege service now holds the keys to impersonate anyone. That violates a basic security principle: a component should hold only the power it actually needs.<\/p>\n<\/li>\n<\/ul>\n<p><strong>With RS256<\/strong>, the picture changes completely:<\/p>\n<ul>\n<li>\n<p>The\u00a0<strong>private key lives in exactly one place<\/strong>\u00a0\u2014 the authentication service. Only it can mint tokens.<\/p>\n<\/li>\n<li>\n<p>The\u00a0<strong>public key is distributed<\/strong>\u00a0to all twelve verifying services. It\u2019s not a secret; if one leaks, nothing is compromised, because a public key cannot forge anything.<\/p>\n<\/li>\n<li>\n<p>Verifying services can do their job \u2014 confirming a token is genuine \u2014 without ever holding the power to create one.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ead\/9f8\/c86\/ead9f8c861bca12707128f5c238a22c7.png\" alt=\"Figure 10. Token Validation in Microservices - Securing the Keys\" width=\"1345\" height=\"660\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/ead\/9f8\/c86\/ead9f8c861bca12707128f5c238a22c7.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ead\/9f8\/c86\/ead9f8c861bca12707128f5c238a22c7.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 10. Token Validation in Microservices &#8212; Securing the Keys<\/figcaption><\/div>\n<\/figure>\n<p>This is the central insight of the section, and it\u2019s worth stating plainly:<\/p>\n<blockquote>\n<p>In a distributed system, use an\u00a0<strong>asymmetric<\/strong>\u00a0algorithm. It lets you separate the power to\u00a0<em>issue<\/em>\u00a0tokens from the power to\u00a0<em>verify<\/em>\u00a0them \u2014 concentrating the dangerous capability in one guarded place while distributing the harmless one freely.<\/p>\n<\/blockquote>\n<h4>ES256: asymmetric, but leaner<\/h4>\n<p><strong>ES256<\/strong>\u00a0stands for\u00a0<strong>ECDSA using SHA-256<\/strong>, where ECDSA is the Elliptic Curve Digital Signature Algorithm.<\/p>\n<p>For decision-making purposes, ES256 belongs in the same category as RS256: it is\u00a0<strong>asymmetric<\/strong>, with a private key for signing and a public key for verifying, and it solves the microservices problem in exactly the same way. The difference is mechanical. ES256 is built on\u00a0<strong>elliptic-curve cryptography<\/strong>, a more modern branch of public-key cryptography that achieves the same security strength with much smaller keys and signatures.<\/p>\n<p>The practical payoff is\u00a0<strong>size<\/strong>. An RS256 signature is large; an ES256 signature, at comparable security, is substantially smaller. Since a JWT travels with every request, a smaller signature means a smaller token on every call. For a high-traffic API, that adds up.<\/p>\n<p>The trade-off is maturity and ubiquity. RSA has been in use for decades and is supported absolutely everywhere; elliptic-curve support, while now excellent, is marginally less universal in older systems. In practice, both are solid choices \u2014 RS256 is the safe, conventional default, and ES256 is the leaner, more modern alternative.<\/p>\n<h4>Putting it side by side<\/h4>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\n<\/th>\n<th>\n<p align=\"left\"><strong>HS256<\/strong><\/p>\n<\/th>\n<th>\n<p align=\"left\"><strong>RS256<\/strong><\/p>\n<\/th>\n<th>\n<p align=\"left\"><strong>ES256<\/strong><\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Family<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Symmetric<\/p>\n<\/td>\n<td>\n<p align=\"left\">Asymmetric<\/p>\n<\/td>\n<td>\n<p align=\"left\">Asymmetric<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Key(s)<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">One shared secret<\/p>\n<\/td>\n<td>\n<p align=\"left\">RSA private\/public pair<\/p>\n<\/td>\n<td>\n<p align=\"left\">EC private\/public pair<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Who can verify?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Only holders of the secret<\/p>\n<\/td>\n<td>\n<p align=\"left\">Anyone with the public key<\/p>\n<\/td>\n<td>\n<p align=\"left\">Anyone with the public key<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Can a verifier also forge tokens?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>Yes<\/strong>\u00a0\u2014 same key signs and verifies<\/p>\n<\/td>\n<td>\n<p align=\"left\">No \u2014 public key can\u2019t sign<\/p>\n<\/td>\n<td>\n<p align=\"left\">No \u2014 public key can\u2019t sign<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Signature size<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Small<\/p>\n<\/td>\n<td>\n<p align=\"left\">Large<\/p>\n<\/td>\n<td>\n<p align=\"left\">Small<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Best fit<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">A single, self-contained service<\/p>\n<\/td>\n<td>\n<p align=\"left\">Distributed systems \/ microservices<\/p>\n<\/td>\n<td>\n<p align=\"left\">Distributed systems where token size matters<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>The decision rule is short. If your application is a\u00a0<strong>single service<\/strong>\u00a0that both issues and verifies its own tokens,\u00a0<strong>HS256<\/strong>\u00a0is simple, fast, and entirely appropriate \u2014 there\u2019s no second party, so a shared secret shares nothing. The moment\u00a0<strong>more than one independent service must verify tokens<\/strong>, switch to an asymmetric algorithm:\u00a0<strong>RS256<\/strong>\u00a0as the well-supported default,\u00a0<strong>ES256<\/strong>\u00a0when you want smaller tokens and your platform supports it. The question is never \u201cwhich algorithm is best\u201d in the abstract \u2014 it is \u201cwho, in my system, needs to verify a token, and should those same parties be able to create one?\u201d<\/p>\n<p>We now know how a token is signed. But verifying a token correctly involves much more than checking that one signature. The next section lays out the full validation checklist \u2014 and shows why skipping any step on it opens a door.<\/p>\n<h3>Validation: The Full Checklist<\/h3>\n<p>Here is a mistake that has shipped to production in countless real systems: a developer wires up JWT authentication, confirms the signature checks out, and considers the job done. The signature is valid \u2014 so the token is trustworthy. Right?<\/p>\n<p>Not quite.\u00a0<strong>A valid signature answers only one question: was this token altered since it was issued?<\/strong>\u00a0It says nothing about\u00a0<em>when<\/em>\u00a0the token was issued,\u00a0<em>who<\/em>\u00a0issued it,\u00a0<em>who<\/em>\u00a0it was meant for, or whether it has been\u00a0<em>cancelled<\/em>\u00a0in the meantime. A token can have a perfect signature and still be one your server must refuse.<\/p>\n<p>Proper validation is therefore a\u00a0<strong>checklist<\/strong>, run in order. This section walks through all six steps, mirroring the security rigor of the earlier parts of this series.<\/p>\n<h4>Why the order matters<\/h4>\n<p>The steps are sequenced deliberately, and the first one comes first for a reason:\u00a0<strong>until the signature is fully verified, you cannot trust a single byte of the payload.<\/strong><\/p>\n<p>Every later check \u2014 expiry, issuer, audience \u2014 reads a value\u00a0<em>from the payload<\/em>. But if the signature hasn\u2019t been confirmed, the payload might be an attacker\u2019s forgery, and an attacker writes their\u00a0<code>exp<\/code>\u00a0claim to say whatever they like. Checking expiry on an unverified payload is theatre. So the signature is verified first; only once it passes does the payload become trustworthy enough to inspect.<\/p>\n<h4>The six-step checklist<\/h4>\n<p><strong>Step 1 \u2014 Verify the signature.<\/strong>\u00a0Recompute the signature from the token\u2019s header and payload (using the secret for HS256, or the public key for RS256\/ES256) and confirm it matches the signature attached to the token. If it doesn\u2019t, the token is forged or corrupted \u2014 reject it immediately and run no further checks. If it matches, the payload is now proven authentic, and the remaining steps can rely on it.<\/p>\n<p><strong>Step 2 \u2014 Check\u00a0<\/strong><code><strong>exp<\/strong><\/code><strong>\u00a0(not expired).<\/strong>\u00a0Read the expiration timestamp and confirm the current time is before it. An expired token is rejected even though its signature is flawless. This is the check that makes a stolen token stop working on its own \u2014 and the entire reason short expiry times are worth setting.<\/p>\n<p><strong>Step 3 \u2014 Check\u00a0<\/strong><code><strong>nbf<\/strong><\/code><strong>\u00a0(not used before valid).<\/strong>\u00a0If the token carries a \u201cnot before\u201d claim, confirm the current time is past it. A token presented before its activation moment is not yet valid and must be refused. In practice this rarely fires, but a complete validator checks it.<\/p>\n<p><strong>Step 4 \u2014 Check\u00a0<\/strong><code><strong>iss<\/strong><\/code><strong>\u00a0(expected issuer).<\/strong>\u00a0Confirm the\u00a0<code>iss<\/code>\u00a0claim names an authentication server you actually trust. A token can be perfectly signed by\u00a0<em>some<\/em>\u00a0issuer, but if it isn\u2019t\u00a0<em>your<\/em>\u00a0issuer, it\u2019s irrelevant \u2014 reject it. This stops your service from honouring tokens minted by an unrelated, untrusted source.<\/p>\n<p><strong>Step 5 \u2014 Check\u00a0<\/strong><code><strong>aud<\/strong><\/code><strong>\u00a0(intended audience).<\/strong>\u00a0Confirm the\u00a0<code>aud<\/code>\u00a0claim names\u00a0<em>your<\/em>\u00a0service. This is the check which is most often skipped \u2014 and skipping it is a genuine vulnerability. Without it, a token issued for the low-privilege analytics service would be accepted by the high-privilege billing service, simply because both trust the same issuer. The\u00a0<code>aud<\/code>\u00a0check is what keeps a token usable\u00a0<em>only<\/em>\u00a0where it was meant to be used.<\/p>\n<p><strong>Step 6 \u2014 Check\u00a0<\/strong><code><strong>jti<\/strong><\/code><strong>\u00a0against a revocation list (if needed).<\/strong>\u00a0For most stateless setups this step is deliberately omitted \u2014 checking a token against a list means consulting a database, which sacrifices the very statelessness JWT was chosen for. But when you genuinely need the ability to cancel an individual token before it expires, this is where it happens: look up the token\u2019s\u00a0<code>jti<\/code>\u00a0and reject it if it appears on a blocklist. This step is the bridge revocation problem, and the reason it\u2019s marked \u201cif needed\u201d is itself the subject of that section.<\/p>\n<h4>What this looks like in code<\/h4>\n<p>Previously we said: build a token by hand once to understand it, then use a library in production. Validation is precisely where that advice earns its keep. A good library performs the entire checklist for you \u2014\u00a0<em>if<\/em>\u00a0you tell it what to expect:<\/p>\n<pre><code class=\"python\">import jwt  # the PyJWT librarydef validate_jwt(token: str, secret: str, expected_audience: str) -&gt; dict:    try:        payload = jwt.decode(            token,            secret,            algorithms=[\"HS256\"],          # Step 1: which algorithm(s) to trust            audience=expected_audience,    # Step 5: the aud claim must match this        )        # Steps 2 and 3 \u2014 exp and nbf \u2014 are checked automatically by decode().        return payload    except jwt.ExpiredSignatureError:        raise AuthError(\"Token has expired\")          # Step 2 failed    except jwt.InvalidAudienceError:        raise AuthError(\"Invalid audience\")           # Step 5 failed    except jwt.InvalidSignatureError:        raise AuthError(\"Signature verification failed\")  # Step 1 failed    # ... and so on for the other failure cases<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Three details in that small function carry the section\u2019s whole lesson.<\/p>\n<p>First,\u00a0<code><strong>jwt.decode()<\/strong><\/code><strong>\u00a0does several checks at once.<\/strong>\u00a0A single call verifies the signature, the expiry, and the \u201cnot before\u201d time. The library runs the checklist \u2014 but only the parts you\u2019ve configured.<\/p>\n<p>Second,\u00a0<strong>a check you don\u2019t ask for doesn\u2019t happen.<\/strong>\u00a0Notice\u00a0<code>audience=expected_audience<\/code>\u00a0is passed explicitly. Leave that argument out, and many libraries simply\u00a0<em>skip the audience check entirely<\/em>\u00a0\u2014 Step 5 silently vanishes, and no error is ever raised. The token validates \u201csuccessfully,\u201d and a real vulnerability sits quietly in your code. The same applies to the issuer. Safe validation means actively telling the library what to expect; its defaults are not your security policy.<\/p>\n<p>Third \u2014 and this is the most important line in the whole snippet \u2014\u00a0<code><strong>algorithms=[\"HS256\"]<\/strong><\/code><strong>\u00a0is not optional.<\/strong>\u00a0It tells the library exactly which signing algorithm(s) to accept. Omitting it, or filling it in carelessly, opens the single most infamous JWT vulnerability of all. We\u2019ve now mentioned it twice as a forward pointer; later we finally show the attack itself.<\/p>\n<h4>The takeaway<\/h4>\n<p>A valid signature is the\u00a0<em>first<\/em>\u00a0line of a six-line checklist, not the whole of it. Full validation confirms a token is\u00a0<strong>authentic<\/strong>(signature),\u00a0<strong>current<\/strong>\u00a0(<code>exp<\/code>,\u00a0<code>nbf<\/code>),\u00a0<strong>from a source you trust<\/strong>\u00a0(<code>iss<\/code>),\u00a0<strong>meant for you<\/strong>\u00a0(<code>aud<\/code>), and\u00a0<strong>not revoked<\/strong>\u00a0(<code>jti<\/code>, when required). A production-grade library will run every one of those checks \u2014 but only the ones you explicitly configure. The unsafe defaults are silent, and silence, in security, is exactly the danger.<\/p>\n<p>That sixth step \u2014 revocation \u2014 has been flagged twice now as something deferred. It\u2019s deferred because it isn\u2019t a simple checklist item; it\u2019s a genuine tension at the heart of the whole JWT design. The next section confronts it directly.<\/p>\n<h3>The Stateless Advantage \u2014 and the Revocation Problem<\/h3>\n<p>This is the most important section in the article, and the most honest. Everything so far has built toward a single payoff \u2014 and that same payoff comes bundled with a single, genuine flaw. A clear-eyed engineer needs both halves. This section gives you both.<\/p>\n<h4>The win: a server that needs no memory<\/h4>\n<p>Return to the very first idea of this series. An API key is a meaningless ticket; to learn what it permits, the server must make a\u00a0<strong>database roundtrip<\/strong>\u00a0to the back room. A JWT prints the details on the ticket itself and stamps them so they can\u2019t be forged.<\/p>\n<p>Now we can state precisely what that buys you. When a JWT arrives, the receiving server checks it using\u00a0<strong>only the token and a verification key the server already holds<\/strong>\u00a0\u2014 the shared secret for HS256, or the public key for RS256\/ES256. The signature confirms the token is authentic; the claims inside answer\u00a0<em>who the user is<\/em>\u00a0and\u00a0<em>what they may do<\/em>. Nothing else is consulted.<\/p>\n<p>That property is\u00a0<strong>statelessness<\/strong>: the server keeps no per-user, per-session memory. It needs no shared session store, no identity database lookup on the request path. And the benefits compound:<\/p>\n<ul>\n<li>\n<p><strong>Speed.<\/strong>\u00a0No network trip to a database before real work begins. Verification is local arithmetic.<\/p>\n<\/li>\n<li>\n<p><strong>Scale.<\/strong>\u00a0Add a hundred new servers and not one of them needs a connection to a session store. Each verifies tokens entirely on its own.<\/p>\n<\/li>\n<li>\n<p><strong>Resilience.<\/strong>\u00a0Recall the microservices picture from figure 2 \u2014 a dozen services each verifying tokens. With JWT, none of them shares a session-store bottleneck. If that store would have existed, it would have been a single point of failure every service depended on. JWT removes it.<\/p>\n<\/li>\n<\/ul>\n<p>This is the killer feature. It is the reason JWT became the default token of the distributed-systems era.<\/p>\n<h4>The problem: you can\u2019t un-issue a token<\/h4>\n<p>Now the honest half.<\/p>\n<p>The very thing that makes a JWT powerful \u2014 it is\u00a0<strong>self-contained<\/strong>, valid purely on its own evidence \u2014 is the thing that makes it dangerous. A stateless server\u2019s logic is simply:\u00a0<em>\u201cthe signature is good and the token hasn\u2019t expired, therefore I trust it.\u201d<\/em>\u00a0That logic has no step that asks anyone\u2019s permission. It never phones home.<\/p>\n<p>So consider: a token is\u00a0<strong>stolen<\/strong>. An attacker copies a user\u2019s valid, unexpired JWT \u2014 through a compromised device, an intercepted request, a leaked log file.<\/p>\n<p>With an old-style server-side session, the fix is instant. You delete the session record from the server. The next request carrying that session is a stranger; access is gone forever.<\/p>\n<p>With a JWT, there is\u00a0<strong>no record to delete.<\/strong>\u00a0The token\u2019s authority lives\u00a0<em>inside the token<\/em>, in the attacker\u2019s possession. Every server that sees it performs the same local check, gets the same answer \u2014\u00a0<em>signature good, not expired<\/em>\u00a0\u2014 and grants access. The token keeps working, in the attacker\u2019s hands,\u00a0<strong>until its\u00a0<\/strong><code><strong>exp<\/strong><\/code><strong>\u00a0timestamp passes.<\/strong>\u00a0And nothing you do can hurry that moment along.<\/p>\n<p>State this plainly, because it is the crux of the entire JWT trade-off:<\/p>\n<blockquote>\n<p>A JWT is\u00a0<strong>self-contained by design<\/strong>, which means it is\u00a0<strong>non-revocable by design.<\/strong>\u00a0The server gave up its memory in exchange for speed \u2014 and a server with no memory has no way to remember that one particular token should now be refused.<\/p>\n<\/blockquote>\n<p>This is not a bug to be patched. It is the direct, unavoidable shadow of the feature. Statelessness and instant revocation are two ends of the same stick: you cannot pick up one end without the other coming with it.<\/p>\n<h4>Living with the trade-off<\/h4>\n<p>You can\u2019t eliminate the problem, but you can\u00a0<em>manage<\/em>\u00a0it. Three patterns are used in practice, and they sit on a deliberate spectrum \u2014 from \u201cfully stateless, accept some risk\u201d to \u201cgive back some statelessness, regain control.\u201d<\/p>\n<h3>1. Short expiry times<\/h3>\n<p>The simplest response: if a stolen token is valid until\u00a0<code>exp<\/code>, then\u00a0<strong>make\u00a0<\/strong><code><strong>exp<\/strong><\/code><strong>\u00a0soon.<\/strong><\/p>\n<p>Set tokens to expire in fifteen minutes rather than twenty-four hours. A stolen token is still unstoppable \u2014 but only for a short window. The damage is time-boxed. This keeps the server fully stateless; you simply accept a small, bounded exposure instead of trying to abolish it.<\/p>\n<p>The objection is obvious, and it\u2019s a usability one: a token that expires every fifteen minutes would log the user out every fifteen minutes. Forcing someone to re-enter their password that often is unworkable. Which is exactly why short expiry never travels alone \u2014 it travels with the next pattern.<\/p>\n<h3>2. The access token + refresh token pattern<\/h3>\n<p>This is the pattern most production systems actually implement, and it resolves the usability objection cleanly by splitting one token into\u00a0<strong>two tokens with two different jobs.<\/strong><\/p>\n<ul>\n<li>\n<p><strong>The access token<\/strong>\u00a0is a JWT, short-lived (say, 15 minutes). It rides along with every API request and does the real authorization work. Being a JWT, it is verified statelessly \u2014 fast, local, no database. If stolen, it\u2019s dangerous for only 15 minutes.<\/p>\n<\/li>\n<li>\n<p><strong>The refresh token<\/strong>\u00a0is long-lived (days or weeks), but it is\u00a0<strong>not<\/strong>\u00a0used for ordinary API calls. Its\u00a0<em>only<\/em>\u00a0power is to ask the authentication server for a fresh access token. It is sent rarely, to one endpoint only, and it is typically a stored,\u00a0<strong>revocable<\/strong>\u00a0credential \u2014 the auth server\u00a0<em>does<\/em>\u00a0keep a record of it.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3ee\/cb1\/b68\/3eecb1b68bc30679fb6835a06fabe56a.png\" alt=\"Figure 11. Access &amp; Refresh Token Split Pattern\" width=\"1346\" height=\"645\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/3ee\/cb1\/b68\/3eecb1b68bc30679fb6835a06fabe56a.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3ee\/cb1\/b68\/3eecb1b68bc30679fb6835a06fabe56a.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 11. Access &amp; Refresh Token Split Pattern<\/figcaption><\/div>\n<\/figure>\n<p>The whole lifecycle is as follows:<\/p>\n<pre><code>1. User logs in.   \u2192 Auth server issues:  a short-lived ACCESS token  +  a long-lived REFRESH token.2. For each API request:   \u2192 Client sends the ACCESS token.  Service verifies it statelessly. Fast.3. After ~15 minutes:   \u2192 The ACCESS token expires. The next API call is rejected.4. Client quietly sends the REFRESH token to the auth server.   \u2192 Auth server checks the refresh token is still valid (and not revoked),     then issues a brand-new short-lived ACCESS token.5. Loop back to step 2. The user notices nothing.<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The elegance is in the division of labour. The token used\u00a0<em>constantly<\/em>\u00a0(the access token) is stateless and fast, exactly where speed matters \u2014 but barely dangerous, because it expires almost immediately. The token that is\u00a0<em>long-lived and genuinely dangerous<\/em>\u00a0(the refresh token) is used\u00a0<em>rarely<\/em>, hits only one endpoint, and\u00a0<strong>can be revoked at that one chokepoint.<\/strong>\u00a0To cut off a compromised user, you invalidate their refresh token: within fifteen minutes their access token expires, the refresh fails, and they\u2019re locked out.<\/p>\n<p>You haven\u2019t achieved truly instant revocation \u2014 there\u2019s still that \u226415-minute tail \u2014 but you\u2019ve shrunk the unstoppable window to something tolerable while keeping the request path stateless. For most systems, that is the right balance.<\/p>\n<h3>3. A revocation blocklist<\/h3>\n<p>When even a fifteen-minute window is unacceptable \u2014 banking, healthcare, anything where a compromised session must die\u00a0<em>now<\/em>\u00a0\u2014 you need true immediate revocation. And here you must pay the honest price.<\/p>\n<p>The pattern: recall the\u00a0<code><strong>jti<\/strong><\/code>\u00a0claim, the token\u2019s unique serial number. To revoke a token immediately, add its\u00a0<code>jti<\/code>\u00a0to a\u00a0<strong>blocklist<\/strong>\u00a0\u2014 a list of \u201ctokens that must now be refused\u201d \u2014 and have every service check incoming tokens against it. To keep that check fast, the list lives in a high-speed in-memory store such as\u00a0<strong>Redis<\/strong>, not a slow disk database.<\/p>\n<p>Be clear-eyed about what this costs. Checking a blocklist means\u00a0<strong>consulting a shared store on every request<\/strong>\u00a0\u2014 which is precisely the database roundtrip that JWT was chosen to eliminate. You have, deliberately,\u00a0<strong>given back the stateless advantage.<\/strong><\/p>\n<p>But the surrender is partial, and that nuance matters:<\/p>\n<ul>\n<li>\n<p>A blocklist holds only\u00a0<em>revoked<\/em>\u00a0tokens \u2014 a tiny set \u2014 not\u00a0<em>every<\/em>\u00a0session. The store stays small.<\/p>\n<\/li>\n<li>\n<p>An in-memory store like Redis is far faster than the relational-database lookup the original API-key model implied.<\/p>\n<\/li>\n<li>\n<p>Entries can be evicted the moment a token\u2019s natural\u00a0<code>exp<\/code>\u00a0passes \u2014 a revoked token already rejected by the expiry check needs no blocklist entry. The list stays small on its own.<\/p>\n<\/li>\n<\/ul>\n<p>So this isn\u2019t a wholesale return to stateful sessions. It\u2019s a precise, conscious concession: trade a little speed, on a small fast lookup, to buy back the immediate-revocation capability \u2014 but only when the application\u2019s risk profile genuinely demands it.<\/p>\n<h4>The honest summary<\/h4>\n<p>JWT\u2019s defining feature and JWT\u2019s defining flaw are\u00a0<strong>the same fact<\/strong>, seen from two sides. A self-contained token lets a server decide\u00a0<em>on its own<\/em>\u00a0\u2014 fast, scalable, memoryless. A self-contained token also cannot be\u00a0<em>un-decided<\/em>\u00a0\u2014 once issued, it is authoritative until it expires.<\/p>\n<p>There is no pattern that makes that flaw vanish. There are only positions on a spectrum:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">Approach<\/p>\n<\/th>\n<th>\n<p align=\"left\">Statelessness<\/p>\n<\/th>\n<th>\n<p align=\"left\">Revocation speed<\/p>\n<\/th>\n<th>\n<p align=\"left\">Typical use<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Short expiry only<\/p>\n<\/td>\n<td>\n<p align=\"left\">Fully stateless<\/p>\n<\/td>\n<td>\n<p align=\"left\">None \u2014 wait for\u00a0<code>exp<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\">Low-risk, simple systems<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Access + refresh tokens<\/p>\n<\/td>\n<td>\n<p align=\"left\">Stateless request path<\/p>\n<\/td>\n<td>\n<p align=\"left\">Bounded \u2014 within the access token\u2019s lifetime<\/p>\n<\/td>\n<td>\n<p align=\"left\">The mainstream default<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>jti<\/code>\u00a0blocklist<\/p>\n<\/td>\n<td>\n<p align=\"left\">Partly stateful again<\/p>\n<\/td>\n<td>\n<p align=\"left\">Immediate<\/p>\n<\/td>\n<td>\n<p align=\"left\">High-stakes systems only<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>The engineering question is never \u201chow do I make JWT revocable?\u201d \u2014 it can\u2019t be made so without giving something up. The question is:\u00a0<strong>how much immediate-revocation power does this system genuinely need, and how much statelessness am I willing to trade for it?<\/strong>\u00a0Answer that honestly, pick your point on the spectrum, and you are using JWT well.<\/p>\n<p>We\u2019ve now covered what a JWT is, what it carries, how it\u2019s signed, how it\u2019s validated, and its central trade-off. One thing remains: the specific ways JWT implementations get attacked \u2014 including the single most infamous JWT vulnerability of all.<\/p>\n<h3>Security Considerations<\/h3>\n<p>A JWT is a security mechanism, which makes its own failure modes worth studying with care. Most JWT disasters are not failures of the cryptography \u2014 the math is sound. They are failures of\u00a0<em>implementation<\/em>: a check skipped, a default trusted, a token stored carelessly. This section covers the four that matter most, beginning with the most famous of them all.<\/p>\n<h4>The algorithm confusion attack:\u00a0alg: none<\/h4>\n<p>We have pointed at this vulnerability twice and promised to show it here. It is the most infamous JWT bug ever, and what makes it memorable is that it turns the token\u2019s own honesty mechanism\u00a0<em>against itself<\/em>.<\/p>\n<p>Recall the structure of the token. The\u00a0<strong>header<\/strong>\u00a0declares which algorithm was used to sign the token, in its\u00a0<code>alg<\/code>\u00a0field \u2014\u00a0<code>\"alg\": \"HS256\"<\/code>. The\u00a0<strong>signature<\/strong>\u00a0is the stamp that proves the token wasn\u2019t altered.<\/p>\n<p>Now recall a detail almost no one expects: the JWT standard defines a\u00a0<em>legitimate<\/em>\u00a0algorithm value called\u00a0<code><strong>none<\/strong><\/code>. It means \u201cthis token is unsigned.\u201d It exists for narrow cases where a token\u2019s integrity is already guaranteed by some other layer. It also creates a trapdoor.<\/p>\n<p>Here is the attack, step by step:<\/p>\n<pre><code>1. The attacker takes a real, valid token and decodes it   (remember - the payload is NOT secret, anyone can read it).2. The attacker EDITS the payload freely:        \"sub\": \"regular-user\"   \u2192   \"sub\": \"admin\"3. The attacker changes the header:        \"alg\": \"HS256\"          \u2192   \"alg\": \"none\"4. The attacker DELETES the signature entirely,   leaving a token that ends in a final dot and nothing after it:        header.payload.5. The token is sent to the server.<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Now everything depends on one line of the server\u2019s code. A\u00a0<strong>naively written verifier<\/strong>\u00a0reads the header, sees\u00a0<code>\"alg\": \"none\"<\/code>, and reasons:\u00a0<em>\u201cThe header says this token is unsigned, so there is no signature to check. No signature to check means nothing fails. Token accepted.\u201d<\/em><\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/71d\/50d\/7e5\/71d50d7e56a35ce4d5db8804c7b44c75.png\" alt=\"Figure 12. JWT Attack with &quot;alg&quot; set to &quot;none&quot;\" width=\"1345\" height=\"642\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/71d\/50d\/7e5\/71d50d7e56a35ce4d5db8804c7b44c75.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/71d\/50d\/7e5\/71d50d7e56a35ce4d5db8804c7b44c75.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 12. JWT Attack with &#171;alg&#187; set to &#171;none&#187;<\/figcaption><\/div>\n<\/figure>\n<p>The forged token sails through. The attacker is now\u00a0<code>admin<\/code>. They never needed the secret key, never broke any cryptography \u2014 they simply\u00a0<strong>let the token tell the server how to verify it<\/strong>, and the token, under attacker control, said \u201cdon\u2019t bother.\u201d<\/p>\n<p>That is the heart of the trap: a naive verifier trusts the\u00a0<em>token\u2019s own header<\/em>\u00a0to decide how the token should be checked. But the header is attacker-controlled. You have let the thing being inspected dictate the terms of its own inspection. (If this sounds like the embedded-public-key problem discussed earlier \u2014 a token vouching for its own trust \u2014 it is exactly the same mistake, in a different costume.)<\/p>\n<p>*<em>The fix is one line, and you have already seen it:<\/em><\/p>\n<pre><code class=\"python\">jwt.decode(token, secret, algorithms=[\"HS256\"])<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>That\u00a0<code>algorithms=[\"HS256\"]<\/code>\u00a0argument is the entire defense. It instructs the library:\u00a0<em>\u201cI will accept HS256 and nothing else. I don\u2019t care what the token\u2019s header claims.\u201d<\/em>\u00a0A token arriving with\u00a0<code>alg: none<\/code>\u00a0is now rejected before any verification logic runs, because\u00a0<code>none<\/code>\u00a0is not on the list the\u00a0<em>server<\/em>\u00a0chose.<\/p>\n<p>The principle, stated generally:<\/p>\n<blockquote>\n<p><strong>Never let the token decide how it should be verified.<\/strong>\u00a0The set of acceptable algorithms is a decision the server makes in advance and enforces \u2014 not a value it reads out of the untrusted header.<\/p>\n<\/blockquote>\n<p>Modern, well-maintained libraries now refuse\u00a0<code>alg: none<\/code>\u00a0by default and require you to specify allowed algorithms \u2014 which is exactly why we insisted on using a real library rather than hand-rolling verification. But the lesson outlives this one bug: a verifier must hold its own policy, never inherit it from the token.<\/p>\n<h4>A second face of algorithm confusion: HS256 vs RS256<\/h4>\n<p>There is a subtler cousin of the same attack.<\/p>\n<p>Recall the asymmetric model: with RS256, the server signs with a\u00a0<strong>private key<\/strong>\u00a0and publishes the\u00a0<strong>public key<\/strong>\u00a0for anyone to verify with. The public key is, by design,\u00a0<em>not secret<\/em>.<\/p>\n<p>Now picture a server configured for RS256 but with a careless verifier that accepts whatever algorithm the header names. An attacker takes the\u00a0<strong>public key<\/strong>\u00a0\u2014 which is freely available \u2014 and uses it as if it were an\u00a0<strong>HS256 shared secret<\/strong>. They forge a token, set the header to\u00a0<code>\"alg\": \"HS256\"<\/code>, and sign it using the public key as the HMAC secret.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/75c\/4af\/226\/75c4af22608d4cc60b74ad70b20d54e2.png\" alt=\"Figure 13. RS256\/HS256 Key Confusion Attack Path\" width=\"1346\" height=\"648\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/75c\/4af\/226\/75c4af22608d4cc60b74ad70b20d54e2.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/75c\/4af\/226\/75c4af22608d4cc60b74ad70b20d54e2.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 13. RS256\/HS256 Key Confusion Attack Path<\/figcaption><\/div>\n<\/figure>\n<p>When the token arrives, the confused server sees\u00a0<code>alg: HS256<\/code>, fetches its RS256 public key as the \u201csecret,\u201d runs the HS256 check \u2014 and it passes. The attacker has forged a valid token using only public information.<\/p>\n<p>The fix is the same fix: pin the accepted algorithm. A server expecting RS256 must specify\u00a0<code>algorithms=[\"RS256\"]<\/code>\u00a0and refuse everything else, so a token claiming\u00a0<code>HS256<\/code>\u00a0is rejected outright. Once again \u2014 the server decides, not the token.<\/p>\n<h4>Secret strength for HS256<\/h4>\n<p>This one is simple and easily neglected. With HS256, the security of every token rests entirely on one shared secret. The signature is only as strong as that secret is hard to guess.<\/p>\n<p>A secret like\u00a0<code>\"secret\"<\/code>,\u00a0<code>\"password123\"<\/code>, or your application\u2019s name is not a secret \u2014 it is an invitation. An attacker who suspects HS256 can run an\u00a0<strong>offline brute-force attack<\/strong>: take any real token they\u2019ve captured, and rapidly try millions of candidate secrets, checking each by recomputing the signature. A weak secret falls in seconds. Once it\u2019s found, the attacker can mint unlimited valid tokens for anyone.<\/p>\n<p>The defense is unglamorous but absolute:<\/p>\n<ul>\n<li>\n<p>Use a\u00a0<strong>long, high-entropy, randomly generated<\/strong>\u00a0secret \u2014 at least 256 bits (32 random bytes), produced by a cryptographic random generator, never typed by a human.<\/p>\n<\/li>\n<li>\n<p>Store it as a\u00a0<strong>secret<\/strong>\u00a0\u2014 in a secrets manager or environment configuration, never hard-coded in source, never committed to version control.<\/p>\n<\/li>\n<li>\n<p>This concern is HS256-specific. With RS256\/ES256 there is no shared secret to guess; the private key is a different kind of object, guarded differently.<\/p>\n<\/li>\n<\/ul>\n<h4>Token storage in the browser<\/h4>\n<p>When a JWT is used to authenticate a user in a web browser, a practical question arises:\u00a0<em>where does the browser keep the token between requests?<\/em>\u00a0This debate is mostly relevant to browser-based apps rather than the pure API-to-API use that is this series\u2019 main focus, so we\u2019ll keep it brief \u2014 but it\u2019s worth knowing the shape of it.<\/p>\n<p>There are two common choices, each with a different weakness:<\/p>\n<ul>\n<li>\n<p><code><strong>localStorage<\/strong><\/code>\u00a0is a simple in-browser storage area. It\u2019s easy to use, but any JavaScript running on the page can read it. If an attacker manages to inject a malicious script into your site \u2014 a\u00a0<strong>cross-site scripting (XSS)<\/strong>\u00a0attack \u2014 that script can read the token straight out of\u00a0<code>localStorage<\/code>\u00a0and steal it.<\/p>\n<\/li>\n<li>\n<p><strong>An\u00a0<\/strong><code><strong>httpOnly<\/strong><\/code><strong>\u00a0cookie<\/strong>\u00a0is a cookie the browser marks as off-limits to JavaScript. Even a successful XSS attack cannot read it directly, which closes the theft route above. The trade-off: cookies are sent by the browser automatically, which can expose the app to a different attack class \u2014\u00a0<strong>cross-site request forgery (CSRF)<\/strong>\u00a0\u2014 that needs its own separate defenses.<\/p>\n<\/li>\n<\/ul>\n<p>There is no universally \u201ccorrect\u201d answer; each option trades one risk for another. The genuinely important point sits underneath both:<\/p>\n<blockquote>\n<p>A JWT is a\u00a0<strong>bearer token<\/strong>\u00a0\u2014 like cash, whoever holds it can spend it. The token itself contains no proof that the\u00a0<em>right<\/em>\u00a0person is presenting it. So the security of any browser-based JWT system depends heavily on simply\u00a0<em>not letting the token get stolen<\/em>\u00a0\u2014 and that depends on the surrounding application being free of injection flaws.<\/p>\n<\/blockquote>\n<h4>Short expiry as a discipline<\/h4>\n<p>The final consideration is not a new attack but a habit that blunts all of them \u2014 and we\u2019ve already met it.<\/p>\n<p>Every threat in this section ends the same way: an attacker obtains a token they shouldn\u2019t have. And we already know JWT\u2019s hard truth \u2014 a stolen token cannot be recalled; it is valid until its\u00a0<code>exp<\/code>.<\/p>\n<p>This makes the expiry time itself a security control. A long-lived token turns any single theft into a long-lived breach. A short-lived one \u2014 minutes, not days \u2014 means a stolen token is a\u00a0<em>briefly<\/em>\u00a0useful prize, paired with the refresh-token pattern so users never feel the churn.<\/p>\n<p>Short expiry doesn\u2019t\u00a0<em>prevent<\/em>\u00a0theft. It\u00a0<strong>caps the cost<\/strong>\u00a0of theft. It is the safety net beneath every other mistake, and it should be treated as non-negotiable discipline rather than a tuning knob.<\/p>\n<h4>The thread connecting all four<\/h4>\n<p>Step back and the four considerations rhyme. The algorithm confusion attack is\u00a0<em>trusting the token to define its own verification<\/em>. Weak HS256 secrets are\u00a0<em>trusting an easily guessed value to be hard<\/em>. Careless token storage is\u00a0<em>trusting a hostile environment to keep a bearer token safe<\/em>. And the antidote running through all of them is the same instinct:\u00a0<strong>trust must be anchored in decisions the server makes and controls \u2014 never in something the token, the attacker, or the environment supplies.<\/strong>\u00a0The cryptography in JWT is rarely what fails. The trust assumptions around it are.<\/p>\n<p>With the failure modes mapped, only the practical verdict remains: when JWT is the right tool for the job \u2014 and when it isn\u2019t.<\/p>\n<h3>When to Use JWT \u2014 and When Not To<\/h3>\n<p>We\u2019ve taken JWT apart completely: its structure, its claims, its signatures, its validation, its central trade-off, and its failure modes. Knowing how a tool works is not the same as knowing when to reach for it. This section is the practical verdict \u2014 a decision guide for an engineer building a real system.<\/p>\n<p>The honest framing is this: JWT is not a\u00a0<em>better<\/em>\u00a0credential than the API key from Part II. It is a\u00a0<em>different<\/em>\u00a0credential, built for a different job. Picking the right one means matching the tool to the situation.<\/p>\n<h4>When JWT is the right choice<\/h4>\n<p>JWT earns its place whenever its defining feature \u2014 a\u00a0<strong>self-contained, statelessly verifiable token<\/strong>\u00a0\u2014 solves a problem you actually have.<\/p>\n<p><strong>Stateless microservices \u2014 the killer use case.<\/strong>\u00a0This is the scenario JWT was built for, and the one where it has no real rival. When an application is split into many small services, and a request may pass through several of them, a JWT lets\u00a0<em>each<\/em>\u00a0service verify the caller independently \u2014 with only a public key, no shared session store, no database roundtrip. The token carries identity and permissions with it. If there is one situation where JWT is unambiguously the answer, this is it.<\/p>\n<p><strong>Cross-domain authentication \u2014 SPAs and mobile apps.<\/strong>\u00a0Recall that JWT emerged partly because session cookies are bound to a single domain and fit modern clients poorly. A JWT is just a string. It travels comfortably from a single-page app or a native mobile application to an API hosted on an entirely different domain, with none of the domain-binding friction cookies impose. When your client and your API don\u2019t share an origin, JWT removes a real obstacle.<\/p>\n<p><strong>Short-lived service-to-service tokens.<\/strong>\u00a0When one internal service needs to call another and prove \u201cthis request is genuinely from me,\u201d a short-lived JWT is an excellent fit. Its built-in expiry means these machine-to-machine tokens clean up after themselves \u2014 no long-lived credential left lying around, no revocation process to run. They are minted, used, and expire, all within minutes.<\/p>\n<p><strong>As a session-scoped layer on top of a long-term API key.<\/strong>\u00a0This is the subtlest fit, and it shows JWT and the API key working\u00a0<em>together<\/em>\u00a0rather than competing. An API key (Part II) is excellent for stable, long-term\u00a0<em>identity<\/em>\u00a0\u2014 \u201cthis is the Acme Corp account.\u201d A JWT is excellent for short-lived, fine-grained\u00a0<em>authorization<\/em>\u00a0\u2014 \u201cthis particular request, in this 15-minute window, may write to invoices.\u201d A common production pattern: a client authenticates\u00a0<em>once<\/em>\u00a0with its long-lived API key, and in exchange receives a series of short-lived JWTs that carry the actual per-request permissions. The API key answers\u00a0<em>who you are over time<\/em>; the JWT answers\u00a0<em>what you may do right now<\/em>.<\/p>\n<h4>When to reach for something else<\/h4>\n<p>JWT\u2019s weaknesses are not hidden and we stated them plainly. Two situations expose them directly, and in both, a different tool is the honest choice.<\/p>\n<p><strong>When you need immediate, reliable revocation.<\/strong>\u00a0This is JWT\u2019s defining flaw, and it is worth refusing to paper over. A standard JWT\u00a0<em>cannot be un-issued<\/em>\u00a0\u2014 it is valid until\u00a0<code>exp<\/code>, full stop. If your system has a hard requirement that a compromised credential must die\u00a0<em>the instant<\/em>\u00a0you say so \u2014 with no fifteen-minute tail, no acceptable window \u2014 then a self-contained token is fighting your requirement.<\/p>\n<p>The alternative here is an\u00a0<strong>opaque token<\/strong>\u00a0checked by\u00a0<strong>introspection<\/strong>: the token is a meaningless reference string (like the API key of Part II \u2014 a coat-check ticket), and the receiving service asks a central authority, on every request, \u201cis this token still valid?\u201d That\u00a0<em>is<\/em>\u00a0a database roundtrip. You are deliberately giving up statelessness \u2014 but in exchange you get exactly the instant, authoritative revocation that statelessness cost you. For high-stakes systems, that is the right trade.<\/p>\n<p><strong>When the token is genuinely long-lived.<\/strong>\u00a0If a credential is meant to last for months \u2014 a stable identifier for a server, a long-running integration \u2014 then JWT\u2019s machinery is working against you. Its central feature, the self-contained expiring claim set, brings no benefit when nothing is meant to expire soon, and its central flaw, non-revocability, becomes a serious liability over a long lifespan. For durable, long-term identity, a plain\u00a0<strong>API key<\/strong>\u00a0is simpler, and \u2014 because it\u2019s checked against a database anyway \u2014 it can be revoked the moment you need to. Don\u2019t reach for a JWT\u2019s complexity to do an API key\u2019s job.<\/p>\n<h4>The decision in one table<\/h4>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">Your situation<\/p>\n<\/th>\n<th>\n<p align=\"left\">Reach for<\/p>\n<\/th>\n<th>\n<p align=\"left\">Why<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Many services must verify a caller independently<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>JWT<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Stateless verification, no shared session store<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Client and API live on different domains<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>JWT<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">A plain string travels anywhere; no cookie domain-binding<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Short-lived, self-expiring service-to-service calls<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>JWT<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Built-in expiry; no revocation process needed<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">Stable, long-term identity for an account or server<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>API key<\/strong>\u00a0(Part II)<\/p>\n<\/td>\n<td>\n<p align=\"left\">Simpler; revocable; nothing needs to expire soon<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">A compromised credential must be killable\u00a0<em>instantly<\/em><\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>Opaque token + introspection<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">True immediate revocation, at the cost of statelessness<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<h4>The underlying judgment<\/h4>\n<p>Reduce all of the above to a single question and it becomes easy to remember:<\/p>\n<blockquote>\n<p><strong>Does this system need a server to decide\u00a0<em>on its own<\/em>, fast and at scale \u2014 or does it need a central authority to stay\u00a0<em>in control<\/em>, able to revoke instantly?<\/strong><\/p>\n<\/blockquote>\n<p>JWT is the tool for the first. It trades central control for local, stateless speed \u2014 a brilliant trade when speed and scale are what you need, a poor one when control is. Opaque tokens and API keys are the tools for the second.<\/p>\n<p>There is no \u201cbest\u201d credential, only a credential that fits. An engineer who can name\u00a0<em>why<\/em>\u00a0they chose JWT \u2014 and name what they gave up to get it \u2014 is using it well. One who reaches for it by default, because it is the modern-sounding option, has skipped the only question that matters.<\/p>\n<p>That leaves one piece of this series unfinished. JWT answers, beautifully, the question\u00a0<em>who are you?<\/em>\u00a0It does not answer a different and harder one:\u00a0<em>how do you let a third party act on your behalf \u2014 without ever handing them your password?<\/em>\u00a0That is the problem the final part was built to solve.<\/p>\n<h3>Conclusion: Three Tools, Three Jobs<\/h3>\n<p>We began this article with a coat-check ticket and we end it with a boarding pass. That shift \u2014 from a meaningless token the server must look up, to a self-describing token the server can read and trust on sight \u2014 is the whole of what JWT contributes. It\u2019s worth gathering the journey into a single picture before we close.<\/p>\n<h4>What JWT actually gave us<\/h4>\n<p>Strip away the Base64URL, the claims tables, the algorithm names, and one idea remains:\u00a0<strong>a JWT moves metadata out of the database and into the token itself, stamped so it cannot be forged.<\/strong><\/p>\n<p>That single move is the source of everything else in this article. It is why a server can be\u00a0<strong>stateless<\/strong>\u00a0\u2014 verifying a request with only a key it already holds, no roundtrip, no shared session store. It is why JWT scales so gracefully across microservices and travels so freely across domains. And it is \u2014 unavoidably \u2014 why a JWT cannot be un-issued: a server that gave up its memory for speed has no memory in which to record that one token should now be refused.<\/p>\n<p>The strength and the flaw are the same fact seen from two sides. An engineer who holds both halves in mind at once understands JWT. One who remembers only the strength will, sooner or later, be surprised by the flaw.<\/p>\n<h4>Three methods, side by side<\/h4>\n<p>This series has now equipped us with three distinct credentials. They are not a ranking \u2014 not worse, better, best. They are three tools for three different jobs:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\n<\/th>\n<th>\n<p align=\"left\"><strong>Basic Auth<\/strong>\u00a0(Part I)<\/p>\n<\/th>\n<th>\n<p align=\"left\"><strong>API Key<\/strong>\u00a0(Part II)<\/p>\n<\/th>\n<th>\n<p align=\"left\"><strong>JWT<\/strong>\u00a0(Part III)<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>What it is<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Username + password sent on every request<\/p>\n<\/td>\n<td>\n<p align=\"left\">A single opaque secret string<\/p>\n<\/td>\n<td>\n<p align=\"left\">A signed, self-describing token<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Carries its own data?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">No<\/p>\n<\/td>\n<td>\n<p align=\"left\">No \u2014 it\u2019s just a reference<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>Yes<\/strong>\u00a0\u2014 identity, permissions, expiry, all inside<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Server needs a lookup?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Yes, every request<\/p>\n<\/td>\n<td>\n<p align=\"left\">Yes, every request<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>No<\/strong>\u00a0\u2014 verified statelessly with a key<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Built-in expiry?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">No<\/p>\n<\/td>\n<td>\n<p align=\"left\">No<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>Yes<\/strong>\u00a0\u2014 the\u00a0<code>exp<\/code>\u00a0claim<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Revocable immediately?<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Yes (change the password)<\/p>\n<\/td>\n<td>\n<p align=\"left\">Yes (delete the key)<\/p>\n<\/td>\n<td>\n<p align=\"left\"><strong>No<\/strong>\u00a0\u2014 valid until it expires<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><strong>Best at<\/strong><\/p>\n<\/td>\n<td>\n<p align=\"left\">Simple, internal, low-stakes access<\/p>\n<\/td>\n<td>\n<p align=\"left\">Stable, long-term identity<\/p>\n<\/td>\n<td>\n<p align=\"left\">Stateless, distributed, short-lived authorization<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>Read the table top to bottom and the trade-off snaps into focus. Basic Auth and the API key keep their information in the database \u2014 which makes them slower per request, but instantly revocable. JWT carries its information in the token \u2014 which makes it fast and stateless, but stubbornly non-revocable.\u00a0<strong>You cannot have both.<\/strong>\u00a0Every authentication design is, at heart, a choice about where the trust lives: in a central store the server must consult, or in the token the server can read on its own.<\/p>\n<p>Choosing well means naming that trade-off out loud \u2014 not reaching for the most modern-sounding option by reflex, but asking what\u00a0<em>this<\/em>\u00a0system actually needs, and what it can afford to give up.<\/p>\n<h4>The one question that remains<\/h4>\n<p>And yet, for all three of these methods, notice what they have in common. Basic Auth, the API key, and the JWT all answer the same single question:<\/p>\n<blockquote>\n<p><strong>\u201cWho are you?\u201d<\/strong><\/p>\n<\/blockquote>\n<p>They are mechanisms of\u00a0<em>authentication<\/em>\u00a0\u2014 proving identity. Each one assumes the party presenting the credential is the party it belongs to, acting on its own behalf.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/451\/4fe\/a9f\/4514fea9fd3fcdd0c0373fa5e8a91794.png\" alt=\"Figure 14. Authentication vs. Authorization\" width=\"1223\" height=\"697\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/451\/4fe\/a9f\/4514fea9fd3fcdd0c0373fa5e8a91794.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/451\/4fe\/a9f\/4514fea9fd3fcdd0c0373fa5e8a91794.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>Figure 14. Authentication vs. Authorization<\/figcaption><\/div>\n<\/figure>\n<p>But a great deal of the modern web does not work that way. When you let a photo-printing service reach into your cloud storage, or allow an analytics dashboard to read your calendar, something more delicate is happening. You are granting\u00a0<em>a third party<\/em>\u00a0access to\u00a0<em>your<\/em>\u00a0resources \u2014 and you would never dream of handing them your password to do it.<\/p>\n<p>None of the three tools in this series solves that. A JWT can prove\u00a0<em>who you are<\/em>\u00a0beautifully; it has nothing to say about how you safely\u00a0<em>delegate<\/em>\u00a0a sliver of your access to someone else.<\/p>\n<p>That problem \u2014\u00a0<strong>how to grant a third party limited access to your resources without ever sharing your credentials<\/strong>\u00a0\u2014 is precisely the problem the next and final part of this series was built around. It is the problem of\u00a0<em>authorization<\/em>\u00a0and\u00a0<em>delegation<\/em>, and the answer is the framework that quietly underpins nearly every \u201cSign in with\u2026\u201d button and connected app on the internet:\u00a0<strong>OAuth 2.0<\/strong>.<\/p>\n<p>We have spent three articles learning, in ever-greater depth, how to answer\u00a0<em>who are you?<\/em>\u00a0It is time to ask the harder question.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1036016\/\">https:\/\/habr.com\/ru\/articles\/1036016\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Why API Keys Aren\u2019t Always EnoughIn Part II we saw that an API key is essentially a long, secret password your software shows to a server. It works, but it has a hidden cost: every time the key is used, the server must look it up in a database to find out what the key is allowed to do, whether it has expired, and whether it has been switched off. A\u00a0JSON Web Token (JWT)\u00a0removes that lookup by carrying all of that information\u00a0inside the token itself. This article explains the problem JWT solves and shows where it sits in the larger story of web authentication.Part I covered Basic Authentication \u2014 sending a username and password with every request. Part II covered API keys \u2014 replacing that reusable password with a single opaque secret string that identifies an application rather than a person.Both approaches share a quiet assumption:\u00a0the server already knows things about the credential, and it has to go and remember them.Think of an API key as a numbered coat-check ticket. The ticket itself tells you nothing \u2014 it\u2019s just a number. To find out what the ticket entitles you to, the attendant has to walk into the back room, find the matching record, and read it. The ticket is meaningless without the back room.That \u201cback room\u201d is a database, and consulting it is called a\u00a0database roundtrip\u00a0\u2014 the server pauses, sends a question to the database, and waits for an answer before it can continue.The hidden cost of a roundtripFor most applications, one database lookup per request is perfectly fine. But consider what the server actually has to check each time an API key arrives:Permissions\u00a0\u2014 what is this key allowed to do?Expiry\u00a0\u2014 is this key still valid, or has it passed its end date?Status\u00a0\u2014 has someone revoked or suspended this key?All three answers live in the database, not in the key. So\u00a0every single request\u00a0triggers a roundtrip before any real work happens.Figure 1. API Keys and Database RoundtripsThis becomes a problem at scale in two specific situations:High request volume.\u00a0A popular API might handle thousands of requests per second. Thousands of identity lookups per second is real, measurable load \u2014 and load that does nothing for the user except verify who they are.Distributed systems.\u00a0Modern applications are often split into many small, independent services (commonly called\u00a0microservices\u00a0\u2014 a single application built as a collection of small, separately running programs that talk to each other). If a request passes through five services, and each one independently re-checks the caller\u2019s identity against the database, that\u2019s five roundtrips for one user action. The database becomes a bottleneck that every service depends on.Figure 2. API Key Validation Scaling ProblemThe deeper issue is\u00a0state. A server that must consult a database to understand a credential is a\u00a0stateful\u00a0server \u2014 it cannot make a decision on its own. It always needs the back room.The core idea behind JWTJWT flips the model. Instead of handing the server a meaningless ticket and forcing it to look up the details, JWT hands the server a ticket that\u00a0has the details printed on it\u00a0\u2014 and stamped in a way that proves the printing wasn\u2019t forged.Figure 3. JWT and Stateless ValidationTo stay with the analogy: a JWT is less like a coat-check number and more like a\u00a0boarding pass. A boarding pass already states your name, your flight, your seat, and your boarding time, right there on the paper. The gate agent doesn\u2019t phone headquarters to look you up \u2014 they read the pass and check that it\u2019s genuine. The information travels\u00a0with\u00a0the traveler.This property has a precise name. A server that can validate a token using only the token itself (plus a verification key it already holds) is\u00a0stateless\u00a0\u2014 it holds no per-request memory and needs no back room. We will unpack exactly\u00a0how\u00a0a JWT proves it hasn\u2019t been forged later, when we open up its three-part structure. For now, the one idea to carry forward is the trade:An API key keeps the metadata in the database and gives you a pointer to it. A JWT puts the metadata in the token and gives you a way to trust it.Figure 4. API Keys vs. JWT comparisonThat trade is powerful, but \u2014 as Part III will explore honestly \u2014 it is not free. Information printed on a token cannot be un-printed, which creates a genuine difficulty when you need to cancel a token early.A Few Words of HistoryEvery technology arrives at a particular moment for a particular reason. JWT is no exception \u2014 it appeared just as the web was shifting away from a model that had quietly dominated for over a decade.The world before: the session cookieFor most of the 2000s, web authentication worked through\u00a0server-side sessions. When you logged in, the server created a record of your session in its own memory or database and handed your browser a small identifier \u2014 a\u00a0session cookie. On every later request, your browser sent that cookie back, and the server looked it up to remember who you were.This is the same coat-check pattern from the previous session above: the cookie is a meaningless number, and the server holds all the real information. It worked well when a website was a single server. But it tied every logged-in user to a specific machine\u2019s memory. Spread your traffic across many servers, and you hit a problem \u2014 a user logged in on Server A is a stranger to Server B, because Server B never created that session record.The pressure that created JWTTwo trends in the early 2010s made server-side sessions increasingly awkward.First, applications stopped being single servers. They were spread across fleets of machines, and increasingly split into\u00a0microservices\u00a0\u2014 many small programs, each handling one job. A shared, central session store became a bottleneck that every service had to consult.Second, the\u00a0clients\u00a0changed. The web was no longer just browsers loading full pages. It was single-page applications (SPAs) \u2014 websites that load once and then behave like desktop apps \u2014 and native mobile apps, often talking to APIs hosted on entirely different domains. Cookies, which are tightly bound to a single domain by design, fit this new world poorly.The industry needed a credential that any server could verify on its own, without a shared session store, and that wasn\u2019t anchored to one domain. The answer was to make the token\u00a0carry its own proof of identity.RFC 7519: the standardThe work happened inside the\u00a0IETF\u00a0(the Internet Engineering Task Force, the body that standardizes the protocols underlying the internet) as part of its OAuth working group \u2014 the same group designing the next generation of authorization standards. They needed a compact, secure token format to pass identity information around, and JWT was built to fill that gap.The result was published in\u00a0May 2015 as RFC 7519\u00a0\u2014 the formal specification that defines what a JSON Web Token is. An\u00a0RFC\u00a0(\u201cRequest for Comments\u201d) is the document format the IETF uses for internet standards; despite the tentative-sounding name, a published RFC is the authoritative definition.A point worth noting: JWT was never a lone invention. It is the centerpiece of a small family of related standards \u2014 collectively called\u00a0JOSE\u00a0(JavaScript Object Signing and Encryption) \u2014 that also define how to sign tokens (JWS), encrypt them (JWE), and represent cryptographic keys (JWK).For most practical purposes, though, \u201cJWT\u201d is the term everyone uses, and it\u2019s the one we\u2019ll use here.From standard to ubiquityA specification only matters if people adopt it, and JWT\u2019s adoption was unusually fast. The identity company\u00a0Auth0, founded in 2013, built much of its product and developer education around JWT and promoted the format heavily \u2014 including\u00a0jwt.io, a free online debugger that let developers paste in a token and see its decoded contents instantly. For a great many engineers, that tool was their first hands-on encounter with the format.From there, JWT spread through the ecosystem. It became the default token format for\u00a0OpenID Connect\u00a0\u2014 the identity layer built on top of OAuth 2.0, and the subject we\u2019ll reach later in this series. Cloud platforms adopted it for service-to-service authentication. Web frameworks in nearly every programming language shipped libraries to create and verify it. Within a few years of RFC 7519, JWT had moved from a new proposal to a default assumption.Why the history mattersThis background isn\u2019t trivia \u2014 it explains the\u00a0shape\u00a0of the technology you\u2019re about to take apart. JWT looks the way it does because it was designed for a specific world: distributed systems with no shared memory, and clients scattered across domains and devices.That origin also explains its central trade-off. JWT was built to let a server make a decision\u00a0on its own, without calling back to a central store. That independence is exactly the feature that makes JWT fast and scalable \u2014 and, as we\u2019ll see, exactly the feature that makes a token hard to cancel once it\u2019s been issued. The strength and the weakness are the same design decision, viewed from two sides.With that context in place, we can now open up an actual token and see what those three dot-separated parts really contain.The Anatomy of a JWTThis is the heart of subject. Once you can see what a JWT actually\u00a0is, every later topic \u2014 claims, signatures, validation, security \u2014 becomes far easier to follow. So we\u2019ll take a real token apart, piece by piece.Three parts, two dotsA JWT, when written out, looks like a long, slightly intimidating string of gibberish:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBZGEifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkIt looks random. It isn\u2019t. Look closely and you\u2019ll spot\u00a0two dots\u00a0dividing it into\u00a0three parts:header  .  payload  .  signatureThat structure never changes. Every JWT, everywhere, is exactly three parts separated by dots:The\u00a0header\u00a0\u2014 describes the token itself.The\u00a0payload\u00a0\u2014 carries the actual information.The\u00a0signature\u00a0\u2014 proves the first two parts haven\u2019t been tampered with.The boarding-pass analogy from the first section holds up neatly here&#8230;.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-479992","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/479992","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=479992"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/479992\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=479992"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=479992"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=479992"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}