Definition
- Token-based authentication – a stateless scheme where the server issues a signed, self-contained token after login, typically a JWT, carrying the user's identity and claims.
- Stateless – the server keeps no per-user session; all identity information travels with the token.
- Authorization header – the client sends the token as
Authorization: Beareron every request. - Signature validation – the server verifies the token by checking its cryptographic signature and expiry, requiring no database lookup.
- Revocation trade-off – because validation is local, a stolen token remains valid until expiry unless a server-side denylist is maintained.
Instead of writing Rajesh's details in a central register, National Bank of India gives him a tamper-proof stamped passbook page that already contains his name, his account type, and an expiry date, all sealed with the bank's official stamp. Any branch can look at the page, verify the stamp is genuine, read the details right there, and serve him without phoning head office. No branch needs to remember Rajesh; the proof travels with him.
Understanding the Concepts
What is token-based authentication?
Token-based authentication is a stateless approach where the server issues a self-contained, signed token after successful login. The token carries the user's identity and claims inside it. The client stores the token and sends it on every request. The server validates the token by checking the signature and expiry without consulting any session store.
The most common token format is JWT (JSON Web Token), which has three Base64Url-encoded parts: Header, Payload, and Signature.
package com.bank.security;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
public class JwtTokenService {
private static final SecretKey KEY = Keys.hmacShaKeyFor(
"national-bank-secret-key-256-bits-long!!!".getBytes());
private static final long EXPIRY_MS = 15 * 60 * 1000; // 15 minutes
public String issueToken(String username, String role) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.issuer("national-bank")
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRY_MS))
.signWith(KEY)
.compact();
}
}
Generated token for rajesh:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYWplc2giLCJyb2xlIjoiQ1VTVE9NRVIi...signature
How does token validation work?
The server receives the token in the Authorization: Bearer header. It splits the token, recomputes the signature over the header and payload using the configured key, and compares it to the signature in the token. If they match, the token is genuine and unmodified. Then it checks the exp claim against the current time.
No database call is needed. This is what makes token-based auth truly stateless.
package com.bank.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
public class JwtValidator {
private static final SecretKey KEY = Keys.hmacShaKeyFor(
"national-bank-secret-key-256-bits-long!!!".getBytes());
public Claims validate(String token) {
return Jwts.parser()
.verifyWith(KEY)
.requireIssuer("national-bank")
.build()
.parseSignedClaims(token)
.getPayload();
}
public static void main(String[] args) {
JwtTokenService service = new JwtTokenService();
String token = service.issueToken("rajesh", "CUSTOMER");
JwtValidator validator = new JwtValidator();
Claims claims = validator.validate(token);
System.out.println("Subject: " + claims.getSubject());
System.out.println("Role: " + claims.get("role"));
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expires: " + claims.getExpiration());
}
}
Subject: rajesh
Role: CUSTOMER
Issuer: national-bank
Expires: 2024-10-27T10:15:00Z
Why is token-based auth better for microservices?
In a microservices architecture with an Account Service, Order Service, and Settlement Service, each service can independently verify the same token using a shared signing key. There is no central session store to become a bottleneck or single point of failure. Adding a new instance requires zero session replication.
With session-based auth, every instance would need access to the session store. With token-based auth, the signing key is all that is needed.
package com.bank.service;
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class AccountServiceController {
private final JwtValidator jwtValidator = new JwtValidator();
@GetMapping("/api/v1/balance")
public String getBalance(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
Claims claims = jwtValidator.validate(token);
String user = claims.getSubject();
String role = claims.get("role", String.class);
// No session lookup. No database call for authentication.
return "Balance for " + user + " (" + role + "): "
+ new BigDecimal("125000.75") + " INR";
}
}
curl -H "Authorization: Bearer eyJhbGci..." /api/v1/balance
--> Balance for rajesh (CUSTOMER): 125000.75 INR
How is token-based auth different from session-based auth?
| Aspect | Token-Based (JWT) | Session-Based |
|---|---|---|
| State | Stateless: token holds identity | Stateful: server stores session |
| Scaling | Any instance validates with the signing key | Needs shared session store or sticky sessions |
| Revocation | Hard: valid until expiry (needs denylist) | Easy: delete session, instant logout |
| Storage | Client holds token | Server holds session record |
| CSRF risk | Low if sent in Authorization header | High because cookie auto-sent |
| Network | No lookup on each request | Lookup on each request |
| Best for | Microservices, SPAs, mobile apps | Server-rendered monoliths |
package com.bank.security;
// Token-based: stateless, no server-side lookup
public class TokenAuth {
public String identify(String jwt) {
Claims claims = Jwts.parser().verifyWith(key).build()
.parseSignedClaims(jwt).getPayload();
return claims.getSubject(); // "rajesh" -- no DB call
}
}
// Session-based: stateful, requires server-side lookup
public class SessionAuth {
public String identify(String sessionId) {
Object user = redisTemplate.opsForValue().get("session:" + sessionId);
return user != null ? user.toString() : null; // DB/Redis call
}
}
What is the revocation problem?
Because validation is local and the token is self-contained, you cannot easily revoke a token before it expires. A stolen token is valid until its exp claim passes.
The mitigations are short-lived access tokens (about 15 minutes) paired with long-lived refresh tokens, and a server-side denylist of revoked token IDs (jti) checked for high-risk actions.
package com.bank.security;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class TokenDenylist {
// In production, use Redis with TTL matching token expiry
private static final Set<String> DENIED = ConcurrentHashMap.newKeySet();
public static void revoke(String jti) {
DENIED.add(jti);
System.out.println("Token " + jti + " added to denylist.");
}
public static boolean isRevoked(String jti) {
return DENIED.contains(jti);
}
public static void main(String[] args) {
revoke("abc-123-token-id");
System.out.println("Is revoked: " + isRevoked("abc-123-token-id")); // true
System.out.println("Is revoked: " + isRevoked("xyz-456-token-id")); // false
}
}
Token abc-123-token-id added to denylist.
Is revoked: true
Is revoked: false
What are common mistakes?
The first common mistake is putting sensitive data in the JWT payload. The payload is only Base64Url-encoded, not encrypted. Anyone who captures the token can decode and read every claim.
// WRONG: sensitive data in JWT payload
Jwts.builder()
.subject("rajesh")
.claim("accountBalance", "125000.75") // Leaks financial data!
.claim("cardNumber", "4532-XXXX-XXXX-1234") // Leaks PII!
.signWith(key).compact();
// CORRECT: only identifiers and roles
Jwts.builder()
.subject("rajesh")
.claim("role", "CUSTOMER")
.claim("iss", "national-bank")
.signWith(key).compact();
// Fetch sensitive data from DB after authorization
The second common mistake is using long-lived tokens without refresh token rotation, turning a single stolen token into permanent access.
What about Spring Security integration?
Spring Security provides BearerTokenAuthenticationFilter that extracts the token from the Authorization header and delegates validation to a JwtDecoder. Spring Boot auto-configures this when spring.security.oauth2.resourceserver.jwt.issuer-uri is set.
package com.bank.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/admin/**").hasAuthority("SCOPE_admin")
.requestMatchers("/api/v1/accounts/**").hasAuthority("SCOPE_accounts:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}));
return http.build();
}
}
What should a senior engineer know?
A senior engineer should know that token-based auth and session-based auth are not mutually exclusive. A common pattern is using JWT for inter-service communication in the backend while using session cookies for the browser-facing frontend.
Senior engineers also know that symmetric signing (HMAC) means every service that validates also knows the secret and could forge tokens. Asymmetric signing (RSA or ECDSA) lets the auth server sign with a private key while other services validate with the public key, which is safer in a zero-trust architecture.
Visual Overview
Login + Token Issuance: ┌────────┐ credentials ┌──────────────┐ │ Client ├───────────────►│ Auth Server │ │(Rajesh)│ │ │ │ │◄───────────────┤ Signs JWT │ │ │ JWT token │ sub=rajesh │ │ │ │ role=CUST │ └────────┘ └──────────────┘ Stateless Validation (any service): ┌────────┐ Bearer JWT ┌──────────────┐ │ Client ├───────────────►│Account Service│ │ │ │ │ │ │ │ Verify sig ✓ │ │ │ │ Check exp ✓ │ │ │◄───────────────┤ No DB call! │ │ │ 200 OK │ │ └────────┘ └──────────────┘
Why Token Auth Scales:
┌────────┐
│ Client │ same JWT token
└───┬────┘
│
┌───┴──────────────┐
│ Load Balancer │
└──┬──────┬──────┬─┘
│ │ │
▼ ▼ ▼
┌──────┐┌──────┐┌──────┐
│Inst 1││Inst 2││Inst 3│ Each has signing key
│Key ✓ ││Key ✓ ││Key ✓ │ Each validates locally
└──────┘└──────┘└──────┘ No shared session store!
Key Takeaway
Token-based authentication is a stateless scheme where, after login, the server issues a signed, self-contained token such as a JWT that carries the user's identity and claims. The client stores it and sends it on every request in the Authorization Bearer header. The server validates the token by checking its signature and expiry alone, keeping no session on the server, which means any instance can verify any token.
In the National Bank of India project, the move from a session monolith to microservices made token-based auth essential. When Rajesh logs in once he gets a signed token, and the Order Service, Account Service, and Settlement Service each verify it independently using a shared signing key. There is no central session database to hammer on every request, removing a major bottleneck and single point of failure.
The scalability benefit is concrete: on a busy trading day, extra Account Service instances spin up in seconds, and because they only need the signing key, they can serve Rajesh immediately without any session replication. A tampered token fails the signature check and an expired token fails the expiry check, both returning 401.
The trade-off is revocation. Because validation is local and the token is self-contained, a stolen token stays valid until it expires. The solution is short-lived access tokens of about fifteen minutes backed by refresh tokens, and for high-risk actions a small server-side denylist of revoked token IDs. JWT claims are only Base64-encoded, not encrypted, so sensitive data like balances must never go in the payload.
Remember one sentence: token-based auth lets any server trust the caller by verifying a cryptographic signature, with no shared state, making it the natural fit for stateless microservices.
---