Back to Authentication & Authorization
Q4Question 4 of 4

Token-Based Authentication (JWT) and Why It Suits Microservices

Key Concepts
  • Token-based auth is stateless: the server issues a signed JWT carrying identity and claims, and keeps no session.
  • The client stores the token and sends it in the Authorization: Bearer header on every request.
  • Validation is local: the server checks the cryptographic signature and expiry without any database lookup.
  • Horizontal scaling is trivial because every instance only needs the signing key to validate any token.
  • The trade-off is revocation: a stolen token is valid until it expires unless a server-side denylist is maintained.
  • Short-lived access tokens (15 minutes) paired with refresh tokens balance security and user experience.

Definition

The token carries the identity inside itself, signed by the server, so any server can trust it without looking anything up.
  • 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: Bearer on 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.

JAVA
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();
    }
}
TEXT
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.

JAVA
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());
    }
}
TEXT
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.

JAVA
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";
    }
}
TEXT
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 |

JAVA
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.

JAVA
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
    }
}
TEXT
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.

JAVA
// 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.

JAVA
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

DIAGRAM
  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        │              │
  └────────┘                └──────────────┘
DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate