PageSourceSearch

https://securoxen.io/assets/what-a-jwt-actually-proves-hGnYufm_.js

js securoxen.io collected 2026-10-02 01:59:07 UTC 7,575 bytes, 1 lines download raw bytes

1import{j as e}from"./index-0qbe4j-K.js";import{P as u}from"./prose-CP3GO64z.js";import{b as p,L as n}from"./react-vendor-CuqwDvgQ.js";import{d as y}from"./logic-kQWaDDM2.js";import{W as x}from"./widget-frame-BWiYKP5E.js";const o="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MjgxIiwiZW1haWwiOiJqb3JkYW5AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NjcyMjU2MDB9.K3bT9xQmR7vL2pN8wZ4aY6cF1dG0hS5jX",g={header:"text-sky-500",payload:"text-fuchsia-500",signature:"text-emerald-500"},m={header:"Says which algorithm signed the token. Supplied by whoever sent it - so a verifier must never trust it.",payload:"The claims. Base64url is an encoding, not encryption - anyone holding this token can read all of it, including that email address.",signature:"The only part that proves anything. Computed over the header and payload with a key, and meaningless until you check it against a key you chose."};function k(){const[a,i]=p.useState("payload"),[h,d,r]=o.split("."),s=y(o),l=a==="signature"?r:s.ok?JSON.stringify(a==="header"?s.header:s.payload,null,2):"";return e.jsxs(x,{title:"Try it: click a part of this token",hint:"all three are readable",children:[e.jsx("p",{className:"mb-3 font-mono text-xs leading-relaxed break-all",children:["header","payload","signature"].map((t,c)=>e.jsxs("span",{children:[c>0&&e.jsx("span",{className:"text-muted-foreground",children:"."}),e.jsx("button",{type:"button",onClick:()=>i(t),"aria-pressed":a===t,className:`${g[t]} rounded px-0.5 transition-colors ${a===t?"bg-muted underline":"hover:bg-muted/60"}`,children:{header:h,payload:d,signature:r}[t]})]},t))}),e.jsx("div",{className:"mb-2 flex gap-2",children:["header","payload","signature"].map(t=>e.jsx("button",{type:"button",onClick:()=>i(t),className:`rounded-md border px-2 py-1 text-xs capitalize transition-colors ${a===t?"border-primary bg-primary/10 text-primary":"border-border text-muted-foreground hover:text-foreground"}`,children:t},t))}),a!=="signature"&&e.jsx("pre",{className:"border-border bg-background mb-2 overflow-x-auto rounded-lg border p-3 text-xs",children:l}),e.jsx("p",{className:"text-muted-foreground text-xs",children:m[a]})]})}function T(){return e.jsxs(u,{children:[e.jsx("p",{children:"A JSON Web Token looks encrypted. It is not. It is three base64url-encoded segments, and anyone holding one can read every claim inside it in about two seconds - no key, no tooling beyond a decoder."}),e.jsx("p",{children:"That single misunderstanding is behind most JWT mistakes: putting things in a token that should not be readable, and trusting a token that has not actually been verified."}),e.jsx("h3",{children:"The three parts"}),e.jsxs("ul",{children:[e.jsxs("li",{children:[e.jsx("strong",{children:"Header"})," - the algorithm and key hint. Note that this is supplied by whoever sent the token."]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Payload"})," - the claims: who the user is, when the token expires, what they may do. Encoded, not encrypted."]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Signature"})," - computed over the first two parts with a secret or private key. This is the only part that establishes anything."]})]}),e.jsxs("p",{children:["Paste one into the ",e.jsx(n,{to:"/tools/jwt-decoder",children:"JWT Decoder"})," and you will see the header and payload as plain JSON. Do that once with a real token from your own application; it makes the point better than any explanation."]}),e.jsx(k,{}),e.jsx("h3",{children:"Encoded is not encrypted"}),e.jsx("p",{children:"Base64url is an encoding, like hexadecimal. It exists to make bytes safe to put in a URL, not to hide them. So a JWT payload is public to anyone who obtains the token: the browser storing it, any extension reading storage, anything that logs the request, and anyone the user forwards it to."}),e.jsx("p",{children:'One piece of pedantry that matters if you meet it: there is an encrypted variant, JWE, whose payload genuinely is unreadable. It is rare, and it is not what anyone means when they say "JWT" - if your token has three parts and the middle one decodes to JSON, it is a signed token and everything above applies.'}),e.jsxs("p",{children:["The practical rule is: ",e.jsx("strong",{children:"never put anything in a JWT you would not print on a postcard."})," Internal user IDs are fine. Roles are usually fine. Email addresses, phone numbers, and anything you would call personal data are a privacy decision you are making implicitly - and one that follows the token everywhere it goes."]}),e.jsx("h3",{children:"The signature is the whole security model"}),e.jsxs("p",{children:["A token's claims mean nothing until the signature has been checked against a key"," ",e.jsx("em",{children:"you"})," chose. Decoding tells you what a token says; verifying tells you whether to believe it. Those are different operations, and confusing them is how tokens get trusted that should not be."]}),e.jsxs("p",{children:["The classic attack makes this concrete. Because the header declares the algorithm, an attacker can change it to ",e.jsx("code",{children:"none"}),", strip the signature, and send it. A verifier that trusts the header instead of its own configuration will accept a token the attacker wrote from scratch - with whatever claims they like."]}),e.jsxs("p",{children:["There is a second, nastier version that still catches real systems, because most libraries now refuse ",e.jsx("code",{children:"none"})," but fewer guard this one. If your service verifies RS256 with a public key, an attacker changes the header to HS256 and signs the token using that public key as the HMAC secret. The key is public, so they have it - and a verifier that picks its algorithm from the header will happily check an HMAC against it and pass."]}),e.jsxs("p",{children:["Both attacks have the same root cause and the same fix: pin the expected algorithm server-side and reject anything else, rather than reading it from the token. You can construct and inspect these cases yourself with the"," ",e.jsx(n,{to:"/tools/jwt-signer",children:"JWT Signer"})," - signing a token, altering a claim, and watching verification fail is the fastest way to internalise what the signature covers."]}),e.jsx("h3",{children:"Expiry is not revocation"}),e.jsxs("p",{children:["The other common surprise: you cannot take a JWT back. Because verification is self-contained - the server checks a signature rather than looking anything up - a valid token stays valid until ",e.jsx("code",{children:"exp"})," passes, even if you delete the user."]}),e.jsxs("p",{children:["That is the trade-off the format makes: statelessness in exchange for revocability. Which is why access tokens should be short-lived, with a refresh token you ",e.jsx("em",{children:"can"})," revoke doing the long-term work. A JWT with a one-year expiry is a password that cannot be changed."]}),e.jsx("h3",{children:"A short checklist"}),e.jsxs("ol",{children:[e.jsx("li",{children:"No personal data in claims - assume the payload is public."}),e.jsx("li",{children:"Pin the algorithm server-side; never trust the header's."}),e.jsxs("li",{children:["Short ",e.jsx("code",{children:"exp"})," on access tokens, revocable refresh tokens behind them."]}),e.jsxs("li",{children:["Check ",e.jsx("code",{children:"exp"}),", and check the issuer and audience while you are there."]}),e.jsxs("li",{children:["Keep signing keys out of your repository - the"," ",e.jsx(n,{to:"/tools/repo-secret-scanner",children:"Repo Secret Scanner"})," will tell you if one is already in there."]})]})]})}export{T as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.