Module 03 · Lesson 4 of 12Free
JWT: stateless authentication tokens
- Level
- Beginner
- Topic
- Authentication, authorization and security
- Reading time
- 15 min
When you finish this page you can
- Explain how a signed JWT lets a server validate identity without looking up a session.
- Trace token verification from untrusted input to an authenticated request.
- Compare local token validation with server-side sessions and revocation checks.
- Identify what logout, expiration and signing-key rotation do not guarantee.
Builds on 2 lessons
Start with lesson 114 minVideo lesson · coming soon
Mina logs out of at 10:02. A copied login token still opens her orders at 10:03, although every server reports that logout succeeded.
For this lesson’s scenario, the token expires at 10:05. How can a server trust a token without looking up a , and what does that mean for logout?
Logout deletes Mina’s token from her browser. Does that also stop another browser using a previously copied token?Show answer
The token carries the evidence
With the session design from the previous lesson, the client carries an identifier. The server looks up what that identifier means.
A JSON Web Token, or , can instead carry signed statements called . This lesson uses signed JWTs, not encrypted JWTs. A signature protects their contents against undetected changes; it does not hide them.1
Diagram not drawn yet: jwt-signed-parts.svg
A signed JWT has three dot-separated parts: encoded header, encoded claims and signature. The header and claims feed signature verification. Reading the claims is separate from verifying them.
The compact signed form has three dot-separated parts: a base64url-encoded header, an encoded payload and an encoded signature. Base64url is a reversible encoding, not .1
Here are illustrative decoded contents, not a usable credential:
{
"alg": "ES256",
"typ": "JWT",
"kid": "kartly-key-a"
}{
"iss": "https://auth.kartly.example",
"sub": "customer_42",
"aud": "kartly-api",
"iat": 1800000000,
"exp": 1800000300
}Kartly’s proposed token contract uses these fields:
| Field | Meaning in this contract |
|---|---|
alg |
Expected signing algorithm |
kid |
Identifier of a configured verification key |
iss |
Issuer: who made the token |
sub |
Subject: which customer it represents |
aud |
Audience: which API may accept it |
iat |
Issue time |
exp |
Expiration time |
The time fields use seconds since the Unix epoch. The example lifetime is 300 seconds. JWT defines these claim names, but applications must decide which claims they require.1
Do not put passwords or delivery addresses in this token. For Kartly’s order , a customer identifier is enough. Extra personal data would travel with every request without helping identify its owner.
The next question is whether moving this evidence into the client solves a problem Kartly actually has.
Local checks replace session lookups
Kartly is at the course’s few-server stage. For this scenario, it runs three app servers behind a .
A shared session store already lets all three recognize Mina. JWTs are therefore an alternative, not a prerequisite for adding servers.
Should Kartly replace shared sessions with locally validated JWTs?
Keep shared sessions for the existing browser application.
Each request resolves an identifier against shared server-side state. Revocation has one place to take effect, but protected requests depend on that store.
Use signed JWTs where independent local validation is a requirement.
Each server checks a signed token using a configured public key. Authentication avoids a session lookup, but changes to login status are not automatically visible.
Why this choice
The simplest working choice is the session system Kartly already has. Three servers do not justify replacing it.
We would change this choice if a separate API needed to validate Kartly-issued credentials without contacting the login system on every request. That API could keep trusted public keys and a validation policy instead of session records.
The cost moves rather than disappears. Kartly must distribute keys and define how long old claims remain acceptable. If immediate revocation is mandatory for every request, adding shared checks removes much of the reason for switching. OWASP explicitly warns that JWT session invalidation reintroduces state.3
For the rest of the lesson, assume Kartly is evaluating that separate API. This is a proposed design, not a claim that its existing sessions need replacement.
Its first task is separating permission to issue tokens from permission to verify them.
Verification does not require signing power
A shared-secret method, such as HS256, gives the issuer and verifier the same secret. Anyone able to verify with that secret can also create tokens. A public-key method separates those capabilities: the issuer signs with a private key, and APIs verify with its public key.3
For this proposed design, Kartly chooses ES256. Only the login component holds the private key. The API servers receive public keys.
This boundary matters if an API server is compromised. Possessing its verification key alone must not let an attacker issue new customer identities. It does not protect customer data already accessible on that compromised server.
A shared secret remains an option when one tightly controlled application both issues and verifies tokens. It has fewer distinct key roles to manage. Here, separate issuance is the requirement that justifies the key pair.
Diagram not drawn yet: jwt-kartly-local-validation.svg
A client logs in through a load balancer to a Kartly app server. The login component signs a token using a private key. The client later sends it through the load balancer to another app server, which verifies using a public key before reading orders from PostgreSQL.
| Component | Responsibility | Talks to | State it keeps | Failure behavior |
|---|---|---|---|---|
| Client | Present a credential | Load balancer | Token copy | Missing token requires login |
| Load balancer | Route requests | App servers | Routing configuration | Requests cannot reach the API |
| Login component | Authenticate and issue | Account database | Private signing key | New logins cannot complete |
| App server | Validate and authorize | Order database | Public keys and policy | Invalid tokens are rejected |
| PostgreSQL | Store accounts and orders | Login and app servers | Durable records | Login or order reads fail |
These are logical responsibilities. The login component can remain inside Kartly’s application deployment.
Issuing and using one token
The proposed write path is:
- The login component verifies Mina’s credentials.
- It creates the required claims from trusted account data.
- A JWT library signs the encoded header and payload.
- The client receives the token over .
The protected read path is:
- The client sends its token through the load balancer.
- The selected app server validates it.
- The order handler uses the validated customer identifier.
- PostgreSQL returns only that customer’s orders.
The app server still needs authorization checks. A valid identity does not grant access to every order.
Local removes one dependency from this path. It does not make the database or application stateless.
Decode first, trust only after validation
Kartly’s proposed presents the token as a : possession is enough to use it. A stolen copy can therefore be replayed.3
GET /v1/orders HTTP/1.1
Host: api.kartly.example
Authorization: Bearer <signed-token>The app server’s authentication middleware owns validation. Configure a maintained JWT library with this policy rather than implementing signature verification yourself:
| Check | Kartly’s required rule |
|---|---|
| Structure | Reject malformed input |
| Algorithm | Allow ES256 only; reject unsigned tokens |
| Key | Select from keys already trusted for Kartly’s issuer |
| Signature | Verify before using claims |
| Issuer | Require Kartly’s exact issuer |
| Audience | Require kartly-api as an accepted audience |
| Claims | Require a nonempty subject and valid numeric times |
| Time | Reject expired tokens; check nbf if present |
Algorithm restrictions, issuer validation and audience validation prevent distinct classes of token substitution. A token’s own header must not decide what algorithms or issuers the application trusts.2
In particular, kid is a lookup hint, not a source of trust. Kartly’s middleware looks it up in its configured key map. It never downloads a key from an arbitrary location supplied by the token.3
Suppose someone edits sub to another customer. The payload changes, so signature verification fails. But a correctly signed token for a different API also needs rejection: its audience does not match Kartly’s policy.1
Kartly’s endpoint contract returns 401 for missing, invalid or expired credentials. After successful authentication, it returns 403 when an operation is forbidden. Verification infrastructure failures produce a temporary server error, not a successful request.
Successful validation establishes what the issuer said. It cannot establish that those statements remain current.
Expiration sets the stale-information window
Assume Kartly issues a token at 10:00 with a five-minute lifetime. At 10:02, support disables Mina’s account.
Under purely local validation, the app server has no new information about that change. It still sees a correctly signed, unexpired token. This follows directly from the proposed design’s inputs.
Find the remaining acceptance window
10:05 expiry − 10:02 disable = 3 minutes
Without another check, requests may continue during that window.
Estimate validation traffic
10,000 requests/s × 60% protected = 6,000 validations/s
The protected fraction is an assumption for this lesson. JWT validation could avoid this many session lookups, not order queries.
The peak request rate comes from Kartly’s example-world stage. The protected fraction and token lifetime are illustrative choices. Neither establishes that signature verification will be faster; Kartly would measure both designs.
How should Kartly stop accepting credentials after logout or account disablement?
Use short-lived tokens and accept delayed revocation.
Local checks stay independent. A copied token can remain usable until its acceptance window closes.
Check a revoked-token record or current account status.
Every protected request checks shared state. Availability and revocation speed now depend on that state and any caching.
Keep shared sessions when immediate logout is the main requirement.
The existing session mechanism remains the central login record. Kartly avoids adding token machinery where local independence is not needed.
Why this choice
Kartly should not promise immediate invalidation and then deploy expiration-only validation.
For an API that accepts a bounded delay, short-lived tokens can satisfy the requirement. For account changes that must apply before the next protected request, require a current shared check or retain sessions.
A revoked-token lookup must be consulted by every relevant validator. Checking only when issuing the next token leaves already-issued tokens unaffected. This is the stateful tradeoff identified by OWASP.3
Short lifetimes also require a renewal plan. A refresh token is a separate credential used to obtain another . In a proposed Kartly design, the login system would store its status and refuse renewal after logout. That would stop new access tokens, not invalidate old ones.
Renewal therefore moves state away from some requests; it does not remove state from login.
Key changes create operational boundaries
For planned rotation, Kartly can first distribute a new public key, then begin signing with its matching private key. It retains the old verification key until tokens signed with it can no longer pass its time policy.
Removing the old key earlier would reject customers whose tokens still have time remaining. This is a consequence of Kartly’s key-selection design.
For this proposal, monitor rejection reasons separately: bad signature, wrong audience, expiration and unknown key. Never include full credentials in logs. A sudden unknown-key increase after deployment points toward inconsistent configuration, not necessarily stolen accounts.
If no trusted key can verify a request, reject it. Do not bypass verification to restore .
Common mistakes
- “JWT means encrypted.” Signed claims remain readable. Keep secrets out of them.3
- “Valid signature means valid request.” Issuer, audience and application permissions still matter.2
- “Deleting the browser copy logs out every copy.” Under Kartly’s local policy, other copies survive until expiration or a shared rejection rule applies.
- “Roles inside the token are current.” In this design they describe issuance-time information. Recheck current permissions where delayed changes are unacceptable.
Where this shows up
- Design: login and single sign-on: coordinates issuance, renewal and logout across applications.
- Design: a payment processing system: must distinguish authenticated identity from permission to move money.
Kartly can now verify its own issuer’s tokens; OAuth 2.0 and OpenID Connect asks how login works when another organization supplies the identity.
Check your understanding
An attacker changes customer_42 to customer_99 and re-encodes the payload. What stops the request?Show answer
A token has a valid Kartly signature but names a different API as its audience. Should Kartly’s API accept it?Show answer
A token expires at 10:05. With no clock allowance, should the API accept it at exactly 10:05?Show answer
Kartly checks account status only when renewing tokens. Does disabling an account immediately stop its current access token?Show answer
Summary
- A signed JWT carries verifiable claims, avoiding a session lookup; immediate logout needs additional state or a different design.
- Signed tokens are readable. Decoding does not validate them.
- Restrict algorithms and trusted keys; validate issuer, audience, required claims and time.
- Local validation accepts issuance-time information until the configured acceptance window closes.
- Choose sessions, local tokens or shared revocation checks from the required logout behavior—not the number of servers.