JWT Authentication, Explained

A JSON Web Token (JWT) is a signed statement your services can verify without calling anyone. This guide takes one apart: header, payload, signature, JWKS, rotation, and the mistakes that actually get apps breached.

How JWT authentication flows: the client logs in once, the auth server signs a token and publishes JWKS, the signed JWT rides every request as a Bearer header, and your API verifies the signature locally

What a JWT actually is

A JSON Web Token (JWT) is a signed, self-contained token with three base64url parts: header, payload, and signature. The server verifies the signature and trusts the claims inside without any session lookup, which makes JWTs stateless. That statelessness is their strength and their main operational risk: revoking a token early takes extra machinery.

A full login system has more layers than the token: how users prove who they are (passwords, passkeys, social login), how that proof turns into something the browser can hold, and how every later request gets checked. The token is the carrier for those last two jobs. Our SaaS authentication guide maps the whole stack; this page takes the token itself apart.

Every authenticated request raises the same questions: who is this user, are they still allowed in, what may they do? A token exists to answer them, and the answers it carries are called claims: small key-value facts about the user and about the token itself. Some claim names are standardized, like sub (which user this is) and exp (when the token stops being valid), and you can add your own. The point of packing the answers into the token is that nothing else needs to be asked: the signature breaks if anyone edits a claim, so a service can read them straight out of the request and trust them, with no database lookup and no call to anyone. That one property, verification without lookups, is what the rest of this page keeps coming back to.

Dissecting an example token

A JWT is three base64url strings joined by dots. The first string is the header, which names the signing algorithm and key id. The second is the payload, which carries the claims. The third is the signature, which covers the other two. Decoding needs no key at all; only verification needs cryptography. Here is a complete example token, taken apart piece by piece.

This token is mechanically real: correctly formed and RS256-signed. But it was signed with a throwaway key generated for this article, so it is an example, not a production credential. No live system trusts it.

text
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDcta2V5LWEifQ
.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c3JfNGY3UWsyTTlwVHpYIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJpYXQiOjE3ODUyMjU2MDAsImV4cCI6MTc4NTIyOTIwMCwicGxhbiI6InBybyIsInJvbGUiOiJhZG1pbiJ9
.U5ndhC7PsyyJ3lgKS2P2VeYMq9DGtP7ZAMYYRMx8ojwfbVOiLAmfkGBvpeJMESKlmh9Qh-Av2W_0Gbgiy9R6j_xx-yzT6qBwtU66zCRSSyJQmcw2IaJDMv778ksVM1yCt6BMSmRVSjJvQ3FutlzM2wzVq3J_MMQcwawsj6Vi41GHDKYZTs7PAyWWO3nRWqFWmMxEmh8Jzj8Bs7yYtQ83Owo3XzRsK7wTa1sckR7Xe_YiDZHxOnds5JmrFz-O06duSX9kmtPfTlGcyh3jeoTnhGTcJ-gRb0yL30bNCPu1Gc0L1KnUGBzB3el6TuP79n9XoXU6rqtG_Ey20IxJgmEAyw

Figure 1A complete JWT exactly as it travels in a request: one line of base64url text, shown wrapped here. The two dots split it into the three parts named above; each part is decoded below.

Token anatomy: the three base64url segments of a JWT decode into a header naming RS256 and a key id, a payload of claims, and a binary RSA signature covering both

The header decodes to:

json
{ "alg": "RS256", "typ": "JWT", "kid": "2026-07-key-a" }

It tells the verifier how to check the signature: the algorithm (RS256) and which key was used (kid, the key id, used to pick the right public key from a key set). Treat the header as a hint from an untrusted source, not an instruction. Your verifier should already know which algorithms it accepts.

The payload decodes to:

json
{
  "iss": "https://auth.example.com",
  "sub": "usr_4f7Qk2M9pTzX",
  "aud": "https://api.example.com",
  "iat": 1785225600,
  "exp": 1785229200,
  "plan": "pro",
  "role": "admin"
}

The first five are registered claims, defined by RFC 7519 itself: iss (who issued it), sub (who it is about), aud (who may accept it), iat (when it was issued), and exp (when it dies). Strictly speaking the spec makes every claim optional: no field is mandatory for a token to be a valid JWT. In practice your verifier sets the law, and production verifiers reject tokens missing exp, iss, or aud.

The last two, plan and role, are private claims: keys you invent, carrying anything your application wants the token to state, as long as the names do not collide with registered ones. This is what makes the format flexible. The token is a small signed document, and you get to design its schema.

A naming aside worth thirty seconds: what everyone calls a JWT is almost always a JWS, a JSON Web Signature. The JWT spec defines the claims format you just read; the JWS wrapper supplies the signature that makes it trustworthy. Unsigned tokens exist in the spec but have no place in production, so in practice JWT means "signed JSON claims".

The signature is what makes the two JSON parts above tamper proof. It is not JSON itself and does not decode to anything readable, because it is raw cryptographic output. It is produced by hashing header.payload and signing that hash with the issuer's private key. Verification reruns the same math from the other side: the verifier hashes the header.payload it actually received, then uses the public key to confirm the signature matches that hash. Change one character anywhere in the first two parts and the recomputed hash no longer matches the signed one, so verification fails. That is the whole tamper-proofing mechanism: nothing is hidden, but only the private key could have produced a signature that checks out.

Two cautions before you play with this yourself. First, base64url is encoding, not encryption: anyone who holds a token can read everything in it, which is why secrets never belong in a payload. Second, the jwt.io debugger is a genuinely useful tool for inspecting tokens, but paste example tokens only. A production token is a live credential, and pasting credentials into websites is how credentials leak. If you want to poke at a token safely, our own client-side JWT decoder never transmits it at all: decoding runs entirely in your browser.

How JWT authentication works end to end

JWT authentication has four moves: the user logs in, the auth server issues a signed token, the client attaches it to every request as a Bearer header, and each service verifies the signature with the issuer's public key. No server keeps a session record. The token is the session.

JWT authentication flow: client logs in, auth server returns a signed JWT, client sends it as a Bearer header, API verifies the signature locally with no session store

Step by step, following the numbered arrows in Figure 3:

  1. Login. The client proves identity once, with a password, a passkey, a magic link, or a social login.
  2. Issue. The auth server builds the claims, signs them with its private key, and hands the token back.
  3. Present. The client sends the token on every API call in the HTTP Authorization header: Authorization: Bearer <token>. The Bearer scheme comes from OAuth 2.0 (RFC 6750), and the name is literal: whoever bears the token gets access.
  4. Verify. The API checks the signature against the issuer's public key, checks the expiry and audience, and authorizes the request. No database round trip, no shared session store.

The verification step is the entire appeal. Verification is local math, so any number of services can authenticate the same request independently. Your API gateway checks the token, your billing service checks the same token, your reporting service checks it again, and none of them talk to each other or to a session store to do it. One login, one token, any number of verifiers.

The word "Bearer" also names the threat model. Possession is proof, so anyone who steals the token is the user until it expires. Transport security and storage discipline are not optional extras; they are the other half of the design.

JWT vs sessions: what stateless actually costs

Sessions came first and still run large parts of the web, so the honest question is why JWTs took over so much of the new work. The short answer: the way we build apps changed. One backend became many services, server-rendered pages became SPAs and mobile clients, and a session row in one server's database stopped being reachable from everywhere a request lands. JWTs made the session portable. The state moved into the token, and any service holding a public key can check it alone. That is the preference in one line; the rest of this section prices it honestly.

Session authentication stores state on the server and pays a database lookup on every request. JWT authentication moves the state into the token and pays a signature check instead. You gain horizontal scale and lose instant revocation: a signed token stays valid until it expires, wherever it travels.

Classic sessions JWT
Where the state lives Server-side session store Inside the token
Cost per request A session-store lookup A local signature check
Revocation Instant: delete the row At expiry, unless you add a denylist
Scaling to many services Every service needs the shared store Every service verifies independently
Frontend-only clients Need a backend to hold the session Work against any API that holds the public key
Best fit One backend, instant-logout requirements Many services, horizontal scale

With classic sessions, the browser holds a random session id in a cookie and the server holds everything else. Revocation is trivial: delete the row, the user is out. The cost is that every service handling a request needs access to that session store, which turns into a shared bottleneck the moment you run more than one backend. It also presumes there is a backend: a frontend-only app calling APIs directly has nowhere to keep a server-side session, which is a big part of why token auth became the default for SPAs and mobile apps.

JWTs invert the trade. Verification needs only the public key, so twenty services can each check tokens without talking to each other or to a central store. This is the scalability win, and it is hard to overstate: adding your twenty-first service adds zero load on your auth infrastructure, because verification never leaves the service doing it. That is why JWTs dominate authentication in microservices: the alternative is every internal service calling home on every request.

Now the honest part. You cannot un-sign a token. If an account is compromised or an employee walks out the door, their access token works until exp. The standard mitigations:

  • Short access-token lifetimes. Minutes, not days. The token in Figure 1 lives one hour; plenty of systems use fifteen minutes.
  • Refresh-token rotation. Real revocation happens at the auth server when the client comes back for a new access token. Kill the refresh token and the session dies at the next renewal.
  • A denylist for the emergencies. For logout-everywhere and incident response, a small cache of revoked token ids (jti claims) checked at the gateway restores instant revocation for the rare cases that need it.

If your system genuinely needs instant, universal revocation on every request, opaque tokens with introspection are the right tool, and pretending otherwise causes pain. More on those below. (And if you would rather not build revocation plumbing at all, that is one of the trade-offs a managed layer absorbs; see the product section at the end.)

Keys, JWKS, and rotation

With RS256 the auth server signs tokens with a private key and publishes the matching public keys as a JSON Web Key Set (JWKS): a plain JSON document listing the issuer's current public keys. Your API is the consumer. Its JWT library fetches the JWKS once, caches it, and picks the right key by the token's kid header every time it verifies a request. That one document is what makes key rotation safe and routine.

The algorithm choice matters more than most tutorials admit. HS256 is symmetric: one shared secret both signs and verifies, so every service that can check a token can also mint one. RS256 (and its elliptic-curve sibling ES256) is asymmetric: the private key signs, the public key verifies, and the public key can be published freely. The moment more than one service verifies your tokens, asymmetric signing is the correct default, which is why every example on this page uses RS256.

Publishing is less ceremony than the word suggests: the issuer simply serves the JWKS (RFC 7517) as a JSON document at a well-known URL, and anyone may read it, because public keys are meant to be public. Here is a realistically shaped example for our example issuer, living at https://auth.example.com/.well-known/jwks.json:

json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "2026-07-key-a",
      "use": "sig",
      "alg": "RS256",
      "n": "qO-yhdCDct4rcxV7gqvsHv7NXNvhGtINeYl6cjxO3_CL...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "2026-01-key-x",
      "use": "sig",
      "alg": "RS256",
      "n": "3f1dYm9pW2kQx8Rt0uVzLc5nHb7Jd2sKfTqGe4wPjZ0M...",
      "e": "AQAB"
    }
  ]
}

Figure 4A JWKS with two active keys, as served at the issuer's well-known URL.

Reading one entry: kty names the key family (RSA), use: "sig" marks it as a signing key, alg pins its algorithm, and n and e are the actual public key, the RSA modulus and exponent. The n values are truncated here with an ellipsis; a real 2048-bit modulus runs to roughly 340 base64url characters. Each key carries its kid, and a verifier matches the token header's kid against the set.

Can an attacker fake this? Not usefully. The keys are public by design, and knowing them does not help forge signatures; only the private key can sign. What an attacker would need is to make your verifier fetch a key set they control, which is why the JWKS URL is configured on your side, fetched over HTTPS, and never taken from the token itself.

The key set in Figure 4 deliberately holds two keys, and that is rotation in action. The issuer has started signing with 2026-07-key-a while 2026-01-key-x stays published, so tokens signed before the switch still verify. Once every old token has expired, the old key disappears from the set. Verifiers never notice; they just keep matching by kid. Figure 5 shows the flow end to end:

JWKS rotation: the auth server publishes public keys at a well-known JWKS URL, services fetch and cache the key matching each token's kid, and old keys stay listed until their tokens expire

One attack class deserves its own paragraph, because most JWT guides skip it: algorithm confusion. The token header claims which algorithm was used, and the header is attacker-controlled. Two classic exploits follow. The first is alg: "none": some early libraries accepted such tokens as validly "signed" with no signature at all. The second is the RS256-to-HS256 downgrade: the attacker forges a token, sets its header to HS256, and signs it with an HMAC whose secret is your public RSA key. A naive verifier that obeys the header then checks the HMAC using the public key it already holds, and the forgery passes. The defense against both is the same line of code: configure your verifier with an explicit algorithm allowlist and never let the token pick.

The other tokens you'll meet

Not every credential is a JWT. Opaque tokens are random strings the issuer resolves, refresh tokens mint new access tokens, API keys identify machines, personal access tokens delegate a user's rights to scripts, and DPoP binds a token to a client key. Each entry below says what the thing is and when your app would actually reach for it.

  • Refresh tokens. These answer a question the short lifetimes above create: if access tokens die in fifteen minutes, why isn't the user logged out every fifteen minutes? Because the client also holds a long-lived refresh token, used only against the auth server, and silently trades it for a fresh access token whenever the current one expires. The user stays signed in for days; a stolen access token stays useful for minutes. Refresh tokens are usually opaque precisely so the auth server can revoke them, and modern practice rotates them on every use, so a stolen one is detected the next time anyone tries to refresh with it. If you use short-lived JWTs, you will use refresh tokens; they are two halves of one design.
  • Opaque tokens. A random string with no internal structure; the issuer keeps the meaning in a database, so checking one means asking the issuer. That lookup buys instant revocation. Reach for them when kill-it-now matters more than distributed verification: admin sessions, high-risk actions, or any single-backend app where the lookup is cheap anyway. Plenty of session cookies and OAuth access tokens are opaque, and that is a legitimate design, not a lesser one.
  • API keys. They identify an application or machine, not a person, for server-to-server calls where nobody is present to log in. No standard structure, no built-in expiry, so treat them like passwords: scope them narrowly, store them in secret managers, rotate them on a schedule.
  • Personal access tokens (PATs). A user-scoped key for scripts and CI, GitHub-style: use one when automation should act as you, with a subset of your rights and an expiry date, instead of a shared anonymous key. Operationally an API key that maps to a human.
  • DPoP and sender-constrained tokens. RFC 9449 (2023) binds a token to a client-held key, so presenting the token also requires proving possession of that key. It converts "whoever bears it" into "whoever bears it and holds the key", which defuses plain token theft. Consider it once stolen-token replay is a real part of your threat model and your client platforms can manage keys.

One line on a related standard you will eventually meet: OAuth 2.0 Token Exchange (RFC 8693) defines how to swap one token for another, for example when a service needs a narrower token to call a downstream service on the user's behalf.

Best practices that actually matter

Most JWT incidents are not broken cryptography. They are skipped verification steps: tokens decoded but never verified, algorithms read from attacker input, missing audience checks, secrets in a readable payload. These nine practices cover the failures that actually happen in production, roughly in the order they bite.

  1. HTTPS everywhere, including the JWKS endpoint. The first question every developer asks about bearer tokens is the right one: what stops someone on the network from reading mine? Only the transport. A bearer token over plain HTTP is a credential broadcast, and a JWKS fetched over an insecure channel means an attacker can hand your verifier their own keys. TLS is not a deployment detail here; it is the other half of the security model.
  2. Verify, don't decode. The classic implementation mistake: most libraries ship a decode() that skips signature checks, meant for debugging. Every year, production systems ship with it on the hot path. If the function name does not say verify, it does not authenticate anything.
  3. Pin your algorithms. Configure the verifier with an explicit allowlist (RS256 here) and reject everything else, including none. This single line closes the whole algorithm-confusion class described above.
  4. Validate iss and aud, always. The signature proves who wrote the token, not that it was meant for you. Without an audience check, a token issued for one service replays perfectly against another that trusts the same issuer.
  5. Keep secrets out of the payload. Anyone holding the token reads everything in it. If you genuinely must transport sensitive data inside a token, JWE (RFC 7516) exists, but the better answer is usually to not put it there.
  6. Store tokens deliberately. In browsers: an httpOnly, Secure, SameSite cookie or in-memory storage. localStorage survives XSS long enough to be exfiltrated, and long-lived tokens there are the classic breach shape.
  7. Expire short, refresh properly. Access tokens in minutes, renewal via rotating refresh tokens. Expiry is your only universal revocation mechanism, so the window it defines should be one you can live with during an incident.
  8. Rotate keys on a schedule. With JWKS the process is boring, which is the point: add the new key, sign with it, retire the old key after the last token it signed expires. Practice it before an emergency makes you.
  9. Watch token size. Tokens ride on every request, headers are commonly capped near 8 KB, and cookies near 4 KB. A permissions array with hundreds of entries belongs in your authorization layer, not in the token.

For the exhaustive version of this list, OWASP's JWT cheat sheet (Java-focused, but the verification guidance is universal) is the reference worth bookmarking.

Libraries in 2026: jose first

Use jose. It is actively maintained, has zero dependencies, runs on Node, browsers, Deno, Bun, and edge runtimes, and verifying against a remote JWKS takes three lines. Pin the current major, jose v6. jsonwebtoken still works in older codebases, but its last release shipped in 2023.

Here is the verification path most services need, in full:

ts
import { createRemoteJWKSet, jwtVerify } from 'jose'; // pin: jose v6

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
);

const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
  algorithms: ['RS256']
});

Those three options are practices 3 and 4 from the list above, enforced in configuration. createRemoteJWKSet handles fetching, caching, and kid selection, including picking up a newly rotated key without a deploy.

On jsonwebtoken, the honest status: it carried the Node ecosystem for a decade, its v9 line shipped in December 2022, and its most recent release dates to 2023. It verifies HS256 and RS256 fine, but JWKS support requires the companion jwks-rsa package, and edge runtimes were never its target. Existing code using it is not broken; new code has a better default. One closing thought on where this all goes. Everything above hardens bearer tokens, but the sharpest edge remains the word bearer: possession is proof. The industry's answer is arriving from two directions at once, sender-constrained tokens like DPoP on the API side, and phishing-resistant login on the user side. If you want to remove the password half of that equation today, start with our guide to implementing passkeys.

The Bridge: one platform,
ready for you

One platform that runs the issuing side of this guide for you: it signs the tokens and rotates the keys, and your services just verify. Two minutes to integrate.

The issuing side, off your plate

Tokens signed for you

Login and signup end in a signed JWT minted by the platform. Key generation, storage, and signing stop being your code.

JWKS published & rotated

Keys live at a JWKS endpoint and rotate without your involvement. Your services verify with the exact jose snippet from this guide.

Every sign-in method

Passkeys, magic links, Google, email and password: every method ends the same way, in a token your services already know how to check.

The gaps this guide kept hitting

Instant revocation

A signed token cannot be un-signed. The Bridge pushes changes instead: revoke a user or change a plan and your app reflects it right away, not at the next renewal.

Claims that mean something

The plan and role claims from the example token live on the user object, arrive in every service already signed, and read from one source of truth.

API tokens for machines

Your users mint their own scoped tokens from a drop-in UI, pick each token's privileges, and revoke them anytime. Stored hash-only.

Common questions

Are JWTs encrypted?
No. A standard JWT is signed, not encrypted. The header and payload are base64url encoded, which anyone can reverse in one line of code. The signature proves the content was not modified; it does not hide it. Encrypted JWTs exist (JWE, RFC 7516) but are rare in practice. The rule that follows: never put secrets in a JWT payload.
Where should I store a JWT in the browser?
Prefer an httpOnly, Secure, SameSite cookie: scripts cannot read it, which takes token theft via XSS off the table, though you then need CSRF protection. The next best option is keeping the token in memory only. Avoid localStorage for long-lived tokens, because any injected script can read it and ship your token elsewhere.
How do I revoke a JWT?
You cannot un-sign one. A valid signature stays valid until the exp claim passes. Practical revocation is built around that fact: keep access tokens short-lived (minutes), do real revocation on the refresh token at the auth server, and for logout-everywhere cases keep a small denylist of revoked token ids that your gateway checks.
When should I use a JWT vs an opaque session token?
Opaque tokens are random strings the issuer looks up, so they revoke instantly but cost a lookup on every check. JWTs verify offline with a public key, which scales across services but delays revocation until expiry. A common production shape uses both: an opaque, revocable refresh token paired with a short-lived JWT access token.
What exactly is a claim?
One key-value fact inside the token's payload. Some names are standardized: sub identifies the user, exp sets the expiry, aud names who may accept the token. You add your own on top, like plan or role. After the signature verifies, claims are the facts your services read and trust without a database lookup.
Is it safe to send a JWT with every request?
Only over HTTPS. The signature stops tampering, but it does not hide the token, and a bearer token is usable by whoever holds it. On plain HTTP you are broadcasting a credential to the network. Over TLS, interception is off the table and the remaining risks are storage and expiry, which the storage and lifetime practices cover.
What is a JWKS and do I need one?
A JSON Web Key Set is the JSON document where an issuer publishes its public signing keys at a well-known URL. If your tokens are RS256-signed, your services already depend on one: the JWT library fetches it, caches it, and picks the right key by the token's kid header. You only host one yourself if you are the issuer.
Do I need refresh tokens too?
In practice, yes. Short access-token lifetimes are your main revocation tool, and refresh tokens are what keep users logged in despite them: the client silently trades a long-lived, revocable refresh token for a fresh access token as each one expires. Skipping them forces a choice between long-lived access tokens or users logging in every few minutes.
Ready to

stop hand-rolling token plumbing?

Auth, billing, and feature flags on one user object. Getting started takes two minutes.

Sign up and start building
Keep building

Explore every identity guide

Every auth question you were saving for later, answered in its own guide. Pick one and go build.