The short answer: JWT authentication works in four moves. You log in with your normal credentials, the server hands you a signed token instead of starting a server-side session, you send that token with every API request, and you get a fresh one before it expires. The token itself is three Base64URL parts — header, payload, signature — defined in RFC 7519, the JSON Web Token standard. The one rule the whole flow depends on: the server must verify the token's signature on every single request. Reading a token's contents proves nothing — anyone can decode it — so a token you merely decoded is never a token you should trust.
The flow, step by step
Here is the complete journey of one login, in the order it actually happens:
| Step | Who acts | What happens | What can go wrong |
|---|---|---|---|
| 1. Log in | Your app | You send your username and password to the server over HTTPS. No token exists yet. | Credentials sent over plain HTTP, or stored in logs, leak before any token is involved. |
| 2. Issue | The server | After checking your password, the server builds a token: a header naming the signing algorithm, a payload of claims about you (who you are, when the token expires), and a signature computed over both with a secret only the server knows. | A weak secret, or an algorithm the token's own header gets to choose, lets attackers forge tokens. |
| 3. Store | Your app | Your app keeps the token somewhere it can attach it to later requests — the storage choice matters, and it is covered below. | Storage in the wrong place exposes the token to cross-site scripting or to other scripts on the page. |
| 4. Send | Your app | Every API request carries the token in an Authorization: Bearer <token> header. |
A token pasted into a URL or a log file travels far beyond the API it was meant for. |
| 5. Verify | The server | On each request the server recomputes the signature, checks the issuer and audience match what it expects, confirms the token is not expired or used too early, and only then serves the request. | Skipping any one of those checks — especially the signature — turns the token into a self-issued permission slip. |
| 6. Expire and refresh | Both | When the short-lived access token expires, your app trades a longer-lived refresh token for a new access token, without asking you to log in again. | Refresh tokens that never rotate and never expire are as good as permanent passwords. |
| 7. Log out | Your app | Your app deletes its copy of the token. The server-side story is more complicated — see the logout section below. | Deleting the client copy alone does not stop a stolen token from working until it expires. |
Notice what the server never does in this flow: it never looks your session up in a database. That is the whole point of a JWT — the token carries the claims, and the signature is what makes them believable. The trade-off is that the server also has nothing to delete when you log out, which is why step 7 deserves its own section. The header convention itself — Authorization: Bearer followed by the token — is the one jwt.io's introduction to JSON Web Tokens uses for exactly this flow.
A worked example: one login, three requests
Follow one user, Maya, through a normal afternoon:
- Login (minute 0). Maya signs in. The server verifies her password and issues an access token valid for 15 minutes, plus a refresh token valid for 7 days. Her app stores the access token in memory and the refresh token in a secure cookie.
- First API call (minute 2). The app requests her dashboard data with the access token in the Bearer header. The server verifies the signature and expiry, sees the request is for Maya's own data, and returns it.
- Second API call (minute 14). Same header, same checks, same result. Nothing about the token has changed — it is simply still inside its 15-minute window.
- Third API call (minute 16). The access token has now expired, so the server answers 401. Maya's app does not show her a login screen: it sends the refresh token to the server's refresh endpoint, receives a brand-new access token (and, in a well-built system, a rotated refresh token too), retries the original request, and Maya never notices.
- Logout (minute 40). Maya clicks Log out. Her app deletes both tokens from the browser. The old access token is already dead by expiry, but the refresh token's fate depends on the server: if the server recorded it, logout can revoke it; if not, it remains usable until its own 7-day expiry — one more reason refresh tokens must be short-lived and revocable by design, not by hope.
Two details in that story are easy to miss. First, the 401 at minute 16 is not an error in the system — it is the system working. Expiry is the mechanism that bounds the damage of a stolen token. Second, the refresh step is invisible to the user only when it is built well; a refresh endpoint that accepts any old token with no rotation and no revocation is the weakest link in the entire flow.
Where to store tokens: the trade-off table
There is no storage option with no downside. The honest choice depends on which attack you are defending against:
| Storage | Survives a page refresh? | Main risk | When it fits |
|---|---|---|---|
| localStorage | Yes | Any JavaScript on the page can read it — a cross-site scripting flaw leaks the token | Simple apps where XSS is well controlled; never for high-value tokens |
| sessionStorage | Only in the same tab | Same JavaScript-readable risk, plus the token dies when the tab closes | Short-lived debugging sessions, not real logins |
| In-memory variable | No | Lost on every refresh, so the app must silently refresh on load | Highest-security browser apps, paired with a rotating refresh token |
| HttpOnly cookie | Yes | JavaScript cannot read it, but the browser sends it automatically — so cross-site request forgery must be handled | Browser apps where the API and the app share a domain, with SameSite cookies and CSRF protection |
The pattern most careful browser apps land on is a split: a short-lived access token in memory, and a rotating refresh token in an HttpOnly, Secure, SameSite cookie. That way a scripting flaw finds no long-lived token to steal, and a forged request still needs to pass the site's CSRF defences. Two rules hold whatever you choose: never put a token in a URL (URLs end up in logs, history, and referrer headers), and never put secrets, passwords, or card numbers in a token's payload — a signed token is readable by anyone who holds it, signature or not.
What makes a good JWT decoder tool
Sooner or later you will need to look inside a token while debugging, and the tool you pick matters: a decoder that uploads your token to a server turns a debugging step into a credential leak. When you compare JWT decoder tools, check four things: that decoding runs entirely in your browser with nothing uploaded, that it shows the expiry status in plain language instead of raw timestamps, that it tolerates a pasted Bearer prefix and stray whitespace, and that it never asks for your signing secret — a decoder has no business with the secret, since decoding needs no key at all. Our free JWT decoder that runs entirely in your browser meets all four, and it flags the usual traps (expired tokens, 13-digit millisecond timestamps, alg: none) as you paste. If you want to check the header your app actually sends rather than the token you think it sends, check the Authorization header your API actually sends — a surprising number of "auth bugs" turn out to be a header that never left the browser.
Why a working flow still returns 401
A 401 means "prove who you are again" — it is not the same as a 403, which means "we know who you are and the answer is still no." MDN's HTTP authentication guide draws that line clearly, and it is the first thing to check when a login flow misbehaves. In JWT flows, the usual 401 causes are: the access token expired and the refresh step failed or never ran; the token was issued for a different API or issuer than the one checking it; the issuing and verifying servers' clocks disagree by more than the small leeway they allow; or the token belongs to another environment entirely (a staging token sent to production is a classic). For the full symptom-by-symptom walkthrough, work through our five-point JWT debugging checklist before changing any code — most of these failures are configuration, not cryptography.
JWT vs session cookies, in flow terms
The difference between the two approaches is really a difference about where the truth lives. With a session cookie, the truth lives on the server: the cookie is just a random ID, and logging out or revoking access is instant because the server deletes the record. With a JWT, the truth lives in the token itself: any server holding the signing key can verify it without a database lookup, which is why JWTs suit APIs and multiple services — but revocation is no longer instant, because there is no record to delete. That is the entire trade in one sentence: JWTs trade instant revocation for stateless verification. If your product needs a "log this user out everywhere, right now" button, you need server-side token tracking (a denylist or a refresh-token registry) alongside your JWTs, or you need sessions after all.
FAQ
How does JWT authentication work, in simple terms?
You log in once, the server gives you a signed token proving who you are, and you show that token with every API request instead of logging in again. The server checks the token's signature each time; if the signature, issuer, audience, and expiry all check out, the request goes through. When the token expires, a refresh token gets you a new one without another login.
Is a JWT the same as a session cookie?
No. A session cookie is a random ID pointing at a session the server remembers; the server can delete it instantly on logout. A JWT carries its own claims and is verified by signature, so no lookup is needed — but the server cannot instantly invalidate a token it has already issued. That revocation gap is the main practical difference between the two.
What happens when a JWT expires?
The server starts answering 401, and your app should trade its refresh token for a new access token and retry the request — the user should not see a login screen for a routine expiry. If there is no refresh token, or the refresh token is itself expired or revoked, only then is a fresh login the correct response.
Can you really log out of JWT authentication?
Only partly, with plain stateless JWTs: deleting the token from the browser stops that browser from using it, but a stolen copy keeps working until it expires. True logout needs help from the server — short access-token lifetimes, a refresh-token registry the server can revoke, or a denylist of tokens checked on each request. Short expiry is what bounds the damage in every design.
What is the best free JWT decoder for debugging?
The best free JWT decoder tools are the ones that decode entirely in your browser without uploading the token, that show expiry in plain language, and that never ask for your signing secret. For day-to-day debugging that is enough: paste the token, read the header and claims, check the expiry — and debug production tokens only after they have expired, or use test-environment tokens instead.
Should I store a JWT in localStorage?
Only if you have accepted its risk with open eyes: any JavaScript running on your page can read localStorage, so a single cross-site scripting flaw exposes the token. For most browser apps, an in-memory access token plus an HttpOnly refresh cookie is the safer split. Never store a token in a URL, and never store secrets in a token's payload.
Why does my API return 401 with a token that looks valid?
Because "looks valid" and "verifies" are different things. The usual causes are expiry, a token issued for another audience or issuer, clock drift between servers, or a token from the wrong environment. Run the five-point debugging checklist linked above before touching the code — most 401s in a working flow are configuration, not cryptography.
Comments
No comments yet. Be the first to share your thoughts below.