Skip to content

JWT Authentication Flow: Access Tokens, Refresh Tokens and Revocation

How JWT authentication works end to end: issuing and validating access tokens, rotating refresh tokens, storing tokens in a browser and revoking them early.

· Founder & developer

Published

Sequence diagram with three lifelines: browser app, auth server and orders API. Login returns an access token and a refresh token. The app calls the API with a bearer token. An opt fragment shows the expired case: 401, refresh request, new token pair, retry.

JSON Web Tokens are popular because an API can check one without calling anyone: verify the signature, read the claims, done. That property is also the source of most JWT problems. A token you cannot look up is a token you cannot recall, and teams discover this the first time a laptop is stolen or an employee leaves.

This post walks through a complete JWT authentication flow with access tokens, refresh tokens and a sequence diagram, then covers the validation checks every API needs, where to keep tokens in a browser, refresh token rotation, key rotation and the realistic options for revocation.

What a JWT is, and what it is not

A JWT (RFC 7519) is a compact, URL-safe string of three base64url parts separated by dots: a header, a payload of claims and a signature. Decoded, an access token looks like this:

{ "alg": "RS256", "kid": "2026-10", "typ": "at+jwt" }
{
  "iss": "https://auth.example.com",
  "sub": "user_8412",
  "aud": "orders-api",
  "scope": "orders:read orders:write",
  "iat": 1793606400,
  "exp": 1793607300,
  "jti": "a3f1c9e2"
}

Three points cause most misunderstandings:

  • A signed JWT is not encrypted. Anyone holding the token can read the payload. Do not put secrets or personal data in it that the holder should not see.
  • JWT is a format, not a protocol. How a client obtains a token is defined elsewhere, usually by OAuth 2.0 and OpenID Connect. The OAuth 2.0 flow diagram covers that part.
  • Not every token in a flow is a JWT. Access tokens often are; refresh tokens are often opaque random strings that only the auth server can look up.

The registered claims you will meet in every token:

Claim Meaning Who checks it
iss Issuer, the auth server that signed the token API
sub Subject, usually the user ID API, for authorization
aud Audience, the API the token is meant for API
exp, nbf, iat Expiry, not-before and issue time, in seconds since the epoch API
jti Unique token ID, useful for denylists API or gateway

The flow, step by step

  1. Sign-in. The user authenticates with the auth server, typically through the OAuth authorization code flow with PKCE.
  2. Token response. The auth server returns a short-lived access token (a JWT, say 15 minutes) and a long-lived refresh token.
  3. API call. The app sends Authorization: Bearer <access token> with each request.
  4. Local validation. The API verifies the signature with the auth server's public key, which it fetched once from the JWKS endpoint and cached, then checks the claims. No call to the auth server per request.
  5. Expiry. After 15 minutes the API answers 401 Unauthorized.
  6. Refresh. The app sends the refresh token to the auth server's token endpoint and gets a new access token and, with rotation, a new refresh token.
  7. Retry. The app repeats the failed request with the new access token.

Steps 3 and 4 are the reason teams pick JWTs: thousands of API requests and no shared session store. Steps 5 and 6 are the price, and where most of the security work sits.

Validating a token: the checklist

Every API that accepts JWTs should run the same checks, in this order:

  1. Allow only the algorithms you expect. Never take the alg header at face value. RFC 8725 describes the two classic attacks: alg set to none, and an RS256 token rewritten as HS256 so a naive library verifies it with the public key as an HMAC secret.
  2. Pick the key by kid from the cached JWKS, and refetch the set when you see an unknown kid, at most once per short interval.
  3. Verify the signature.
  4. Check iss and aud. A token issued for another API must fail here.
  5. Check exp and nbf with a small clock tolerance, such as 60 seconds.
  6. Check the token type. RFC 9068 access tokens carry typ: at+jwt, which stops an ID token from being replayed as an access token.
  7. Then authorize: scopes, roles and ownership of the resource.

With the jose library in Node.js, most of the list is one call:

import { createRemoteJWKSet, jwtVerify } from 'jose'

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

export async function verifyAccessToken(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://auth.example.com',
    audience: 'orders-api',
    algorithms: ['RS256'],
    typ: 'at+jwt',
    clockTolerance: 60,
  })
  return payload // scopes and sub are checked by the caller
}

How long should an access token live?

The lifetime is the window in which a stolen token works and cannot be recalled. Shorter means safer and more refresh traffic. For a user active for an eight-hour working day:

Access token lifetime Exposure if stolen Refreshes per user per day
5 minutes 5 minutes 96
15 minutes 15 minutes 32
1 hour 1 hour 8
24 hours 24 hours 1

Most teams land between 5 and 15 minutes for access tokens. A day-long access token is a session cookie that you cannot revoke.

Refresh token rotation

A refresh token lives for days or weeks, so leaking one is worse than leaking an access token. RFC 9700, the OAuth security best current practice, requires one of two protections for refresh tokens held by public clients such as browser apps and mobile apps:

  • Sender-constrained refresh tokens, bound to a key the client holds, with mutual TLS or DPoP.
  • Rotation: every refresh returns a new refresh token and invalidates the old one, while the auth server remembers the chain. If an attacker and the real client both use the same token, one of them presents an invalidated token, and the server revokes the active refresh token, which ends the chain for both.

Rotation turns a stolen refresh token from a weeks-long problem into one that ends the next time the legitimate client refreshes. The cost: the user has to sign in again, and your token endpoint needs to handle two tabs refreshing at the same time without tripping reuse detection, usually with a short grace period.

Where to keep tokens in a browser

Storage Readable by JavaScript Main risk Notes
localStorage Yes Any XSS can steal tokens Survives reloads; avoid for refresh tokens
Memory only Yes, while the page lives XSS during the session Lost on reload; refresh on startup
HttpOnly cookie No CSRF Use Secure, SameSite, and CSRF tokens for state-changing requests
Backend for frontend No; tokens stay on the server Server-side session handling The browser holds only a session cookie

The IETF's draft guidance on OAuth for browser-based applications describes the backend-for-frontend pattern as the option that keeps tokens away from JavaScript entirely. If you keep tokens in the browser, keep the access token in memory and the refresh token in an HttpOnly cookie scoped to the token endpoint.

Revocation: what actually works

You cannot unsign a JWT. You can only make the API stop accepting it, and each option gives back some statelessness:

Strategy How it works Cost
Short expiry Tokens die on their own in minutes None; the exposure window stays
Revoke the refresh token The next refresh fails, so access ends at the next expiry One lookup at the token endpoint, which already has state
Denylist by jti Gateways reject listed IDs until their exp A shared cache, checked on every request
Per-user cutoff Reject tokens issued before a stored timestamp for that user One lookup per user, cacheable
Introspection The API asks the auth server about each token (RFC 7662) A network call; use it for high-risk endpoints only

A practical combination: access tokens of 15 minutes or less, rotating refresh tokens revoked on logout and password change, and a per-user cutoff for the rare "sign out everywhere now" case.

Rotating signing keys

Keys leak and algorithms age, so plan rotation from day one. Publish the new public key in the JWKS a day or so before you sign with it, so every API cache has it. Switch signing to the new kid. Keep the old key in the JWKS until the last token signed with it has expired, then remove it. An API that refetches on an unknown kid survives an emergency rotation without a restart.

The JWT flow diagram

Sequence diagram with three lifelines: browser app, auth server and orders API. Login returns an access token and a refresh token. The app calls the API with a bearer token. An opt fragment shows the expired case: 401, refresh request, new token pair, retry.

5 components · 12 connections

Open in Archboard
Diagram contents
  • Browser app
  • Auth server
  • Orders API
  • access token expired
  • 1. Sign in (OAuth code flow + PKCE)
  • 2. Access token (JWT, 15 min) + refresh token
  • 3. GET /orders, Authorization: Bearer JWT
  • GET /.well-known/jwks.json (once, then cached)
  • public keys
  • 4. 200 OK
  • 5. GET /orders (expired JWT)
  • 6. 401 Unauthorized
  • 7. POST /token (refresh token)
  • 8. New token pair, old refresh token invalidated
  • 9. Retry GET /orders with new JWT
  • 10. 200 OK
A browser app signs in, calls an API with a short-lived JWT access token, and uses a rotating refresh token when the access token expires. The API validates tokens locally with keys from the auth server's JWKS endpoint.

How to read it:

  • Three lifelines. The browser app on the left, the auth server in the middle and the orders API on the right. Time runs downward.
  • Login and token response at the top: the app gets an access token and a refresh token.
  • The happy path: the bearer request to the API and the response. The API validates the token with cached JWKS keys and never calls the auth server.
  • The opt fragment covers expiry: 401, refresh, a new token pair with the old refresh token invalidated, and the retried request.
  • The Markdown block shows the decoded token and the validation checks.

Drawing auth flows that reviewers can check

  • Name every token on every arrow, with its type and lifetime: "access token (JWT, 15 min)". Ambiguity about which token travels where hides real bugs.
  • Show where validation happens. Put a note on the lifeline that verifies the signature, and say where its keys come from.
  • Draw the failure paths. Expiry, refresh reuse and logout belong in fragments; most incidents start there.
  • Keep secrets out of the picture. Use placeholder values; diagrams get shared further than you expect.

The sequence diagram example covers lifelines and fragments in detail, and the zero trust architecture diagram shows where token checks sit in a wider access model.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. Add three Lifeline nodes from the Sequence pack: Browser app, Auth server and Orders API.
  3. Draw the messages as edges between lifelines, top to bottom: login, token response, the bearer request and its response. Label each with the token it carries.
  4. Add a Fragment from the Sequence pack with the operator opt around the expiry path: 401, refresh request, new token pair, retry.
  5. Add a Markdown block from the Core pack with the decoded token and the validation checklist.

The example uses 5 diagram nodes, within the Free plan.

FAQ

Are JWTs encrypted? Signed JWTs are not. The payload is base64url-encoded and readable by anyone who has the token. JSON Web Encryption exists for encrypted tokens, but most access tokens are signed only.

Should I store a JWT in localStorage? Avoid it for anything long-lived. Any cross-site scripting bug can read localStorage. Keep access tokens in memory and refresh tokens in an HttpOnly cookie, or keep tokens on a backend for frontend.

How do I log out a user when JWTs are stateless? Revoke the refresh token so no new access tokens are issued, and keep access tokens short. For immediate effect, add a per-user cutoff timestamp or a jti denylist that your API checks.

What is the difference between an ID token and an access token? An ID token tells the client who signed in and is meant for the client. An access token is meant for an API. APIs should reject ID tokens; the typ and aud checks make that explicit.

Should I use HS256 or RS256? HS256 uses one shared secret, so every service that verifies tokens can also mint them. RS256 or ES256 lets APIs verify with a public key while only the auth server holds the private key, which is the safer default for more than one service.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.