logo

Spring Boot Authorization Service: Stateless JWT Authentication

Stateless JWT

Authorization Service

Spring Boot 4.0.3 • RSA256 Asymmetric Keys • JWKS • Token Rotation

A complete guide to issuing, validating and revoking JWTs across microservices

By Bharat Chaudhari July 16, 2026

01 Overview

Authentication is one of the few architectural concerns that affects every request flowing through a distributed system.

Every service must answer the same question:

“Can this request be trusted?”

In monolithic applications, authentication is often implemented through server-side sessions stored in memory or databases. While effective for smaller systems, this model becomes increasingly difficult to scale as services become distributed, and infrastructure becomes dynamic.

Modern microservice architectures require a different approach.

Rather than maintaining centralized session state, services authenticate requests using cryptographically signed tokens that can be validated independently by every service in the platform.

This approach eliminates session dependencies, improves scalability, and allows authentication infrastructure to evolve independently from business services.

In this article, we’ll explore how a dedicated Authorization Service issues and manages JWT access tokens using asymmetric cryptography, enabling secure and scalable authentication across a Spring Boot microservices platform.

Authentication Vs Authorization

One of the most common sources of confusion in software architecture is the distinction between authentication and authorization.

Authentication answers:

  • “Who are you?”

Authorization answers:

  • “What are you allowed to do?”

These concepts are related but fundamentally different.

Examples:

Authentication

  • Verify username and password
  • Validate identity provider response
  • Issue access token

Authorization

  • Can this user create payments?
  • Can this user access admin endpoints?
  • Can this user view reports?

The Authorization Service focuses on authentication and token issuance. Business services remain responsible for enforcing authorization decisions. This separation creates cleaner ownership boundaries and simplifies security management.

03 Why We Built A Dedicated Authorization Service

Many organizations initially implement authentication directly within application services. While convenient in the beginning, this approach creates several challenges:

  • Duplicate security logic
  • Multiple token implementations
  • Inconsistent authentication rules
  • Difficult key management

A dedicated Authorization Service centralizes trust management. Its responsibilities include:

  • Credential validation
  • Token issuance
  • Token revocation
  • Public key distribution

Every other service consumes authentication outcomes rather than implementing authentication infrastructure independently. This creates a single source of trust across the platform.

04 Why JWT Instead Of Server Sessions?

Traditional session-based authentication relies on server-side state.

A user logs in. The server creates a session. Every subsequent request reference that session.

While effective in monolithic systems, this approach introduces challenges in distributed environments. As services scale horizontally, session state must be shared through:

  • Databases
  • Distributed caches
  • Sticky load balancing

These dependencies increase operational complexity.

JWT authentication eliminates this requirement. Instead of storing session information centrally, user identity and claims travel with the request inside a cryptographically signed token.

Every service can independently validate the token without contacting the Authorization Service. This dramatically improves scalability while reducing infrastructure dependencies.

05 Why Rs256 Instead Hs256?

This deserves its own section because it is one of the most important architectural decisions on the platform.

JWT signing algorithms generally fall into two categories:

HS256 (Symmetric)

The same secret signs and validates tokens. This creates a challenge. Every service validating tokens must possess the signing secret. If any service is compromised, token issuance trust is compromised.

RS256 (Asymmetric)

The Authorization Service owns the private key. Only the Authorization Service can create valid tokens. All other services receive only public keys. They can validate tokens but cannot generate them.

This dramatically improves security while simplifying trust distribution. For distributed systems, asymmetric cryptography is generally the preferred approach.

06 Why JWKS Matters

A common question is: How do services obtain the public key?

The answer is JWKS.

JSON Web Key Sets provide a standard mechanism for publishing public signing keys. Rather than manually distributing keys to every service, the Authorization Service exposes a JWKS endpoint. Services automatically retrieve public keys when needed.

Benefits include:

  • Simplified onboarding
  • Automated key distribution
  • Reduced operational effort
  • Improved security

This allows authentication infrastructure to evolve without requiring changes across business services.

07 Problem Statement

What is the real-world problem?

In a distributed microservices architecture, every service that needs to know “who is this user?” traditionally calls back to an auth server on every request, or worse, each service maintains its own session store. This creates tight coupling, a network hotspot, and a single point of failure.

Why are existing solutions insufficient?

  • Session stores (Redis, DB) require every service to share state — negating horizontal scale benefits.
  • Symmetric HMAC JWTs (HS256) require every verifying service to hold the shared secret, making secret rotation a distributed coordination problem.
  • OAuth2 introspection endpoints add a synchronous network hop per request, reintroducing the coupling you tried to remove.

Where does this problem typically occur?

  • API Gateways that must authenticate inbound traffic before routing upstream services.
  • Internal service-to-service calls where a downstream service needs to know the caller’s identity and roles.
  • Multi-tenant SaaS platforms where token isolation per tenant is critical.

08 Solution Overview

High-level explanation

A dedicated Spring Boot Authorization Service owns the RSA private key. It issues short-lived Access Tokens (15 min) and longer-lived Refresh Tokens (7 days). Downstream services fetch the public key from the standard JWKS endpoint and verify all subsequent tokens locally – zero network calls per request.

Key components involved

ComponentRole
JwtServiceSigns access tokens with the RSA private key; verifies with the public key.
SecurityFilterChainEnforces stateless session policy; routes public vs. protected endpoints.
JwkControllerExposes the public key in JWKS format for downstream consumers.
RevokedAccessTokenServiceMaintains a SHA-256 deny-list with scheduled cleanup.
AuthControllerREST API surface: /login, /refresh, /logout.
Flyway + PostgreSQLVersion-controlled schema for revoked tokens and refreshed token records.
Distributed TracingUses Micrometer Tracing and Zipkin (via common-module) to append trace IDs across all authentication requests.


Why this approach works

  • Asymmetric keys: private key never leaves the auth service; any consumer can verify without a secret.
  • Stateless tokens: access tokens carry all identity claims – no DB lookup per request for normal traffic.
  • Short-lived access + rotated refresh: limits blast radius of a compromised token.
  • SHA-256 hashed deny-list: revocation without storing raw tokens; automatic expiry cleanup keeps the table lean.

09 Authorization Flow

				
					  User Login 
                         │ 
                         ▼ 
 
               Authorization Service 
 
                         │ 
 
          Validate Credentials 
 
                         │ 
 
                         ▼ 
 
                 Generate JWT 
 
                         │ 
 
                         ▼ 
 
                   Return Token 
 
                         │ 
 
                         ▼ 
 
                    API Gateway 
 
                         │ 
 
                         ▼ 
 
                 Business Services 
 
                         │ 
 
         Validate Using Public Key 
 
                         │ 
 
                         ▼ 
 
                  Process Request
				
			

10 Architecture/Design

Components

  • AuthController: HTTP entry point. Thin layer; delegates entirely to AuthenticationService.
  • AuthenticationService: Orchestrates credential validation, token generation, and refresh rotation.
  • JwtService: Decodes RSA keys on startup. Generates and validates RS256-signed JWTs.
  • JwkController: Exposes RSA public key as a JWKS JSON response on /.well-known/jwks.json.
  • JwtAuthFilter: Per-request filter. Extracts Bearer token, validates, populates SecurityContext.
  • RevokedAccessTokenService: Deny-list with SHA-256 hashing and nightly @Scheduled cleanup.
  • SecurityConfig: Declares stateless session policy, public routes, and filter chain order.
  • Flyway + PostgreSQL: Schema migrations for revoked_access_tokens and refresh_tokens tables.

Any service depending on the Common Module will automatically start reporting distributed traces to Zipkin without writing custom configurations in every service. 

11 Why Services Validate Tokens Locally

One of the most important architectural decisions in this platform is local token validation.

Many teams initially design systems where every request requires a call back to the authentication service.

Example:
Request -> Service -> Auth Service -> Validation -> Response

This creates a bottleneck. If the Authorization Service becomes unavailable, every request in the platform fails.

Local JWT validation eliminates this dependency. Services validate tokens mathematically using the public key. No network call is required.

This creates:

  • Lower latency
  • Better scalability
  • Improved resilience

Most importantly, temporary Authorization Service outages do not impact already authenticated users.

12 Token Revocation Trade-Offs

JWTs provide significant scalability benefits. However, they introduce an important trade-off.

A valid token remains valid until expiration. This means revocation is not instantaneous unless additional mechanisms are introduced.

Common strategies include:

  • Short-Lived Tokens: Reduce risk through limited token lifetime.
  • Refresh Tokens: Issue new access tokens periodically.
  • Revocation Lists: Maintain explicit invalidation records.
  • Token Versioning: Invalidate tokens through user state changes.

The correct approach depends on security requirements and operational constraints.

13 Key Rotation Strategy

Cryptographic keys should never be considered permanent. Production environments require periodic key rotation.
A common approach is:

  1. Generate a new key pair.
  2. Publish new public key via JWKS.
  3. Continue supporting the previous key temporarily.
  4. Retire the old key after expiration window.

This allows key replacement without disrupting authenticated users. Key rotation is often overlooked in tutorials but is essential for long-term security.

14 Core Components

JwtService

Purpose: Centralizes all JWT cryptographic operations — generation, parsing, and validation.

Role: Decodes Base64 PKCS#8 private key and X.509 public key at startup. Signs tokens with the private key; verifies incoming tokens with the public key. Provides hashToken() for deny-list storage.

Key considerations:

  • KeyFactory.getInstance("RSA") is the standard Java way to load RSA keys.
  • Always validate both signature AND expiration.
  • Never expose the private key outside of this service.

SecurityConfig (SecurityFilterChain)

Purpose: Defines Spring Security’s HTTP security rules for the auth service itself.

Role: Disables CSRF (stateless REST API). Marks /api/v1/auth/login, /api/v1/auth/refresh, and /.well-known/* as public. Enforces STATELESS session creation policy. Register JwtAuthFilter before UsernamePasswordAuthenticationFilter.

Key considerations:

  • SessionCreationPolicy.STATELESS is non-negotiable — any session creation silently breaks statelessness.
  • Custom CustomAuthenticationEntryPoint returns a structured JSON 401 instead of Spring’s default HTML error.

JwkController

Purpose: Allows any downstream service or API Gateway to obtain the RSA public key without contacting the Auth Service per request.

Role: Extracts the RSAPublicKey modulus (n) and exponent (e), Base64url-encodes them, and returns a standard JWKS JSON payload.

Key considerations:

  • Consumers should cache the JWKS response (e.g. 1-hour TTL).
  • Only re-fetch on a cache miss caused by an unknown kid.
  • This eliminates per-request latency completely.

RevokedAccessTokenService

Purpose: Enables token revocation in a stateless JWT architecture.

Role: On logout, stores SHA-256(rawToken) + expiresAt in the revoked_access_tokens table. JwtAuthFilter checks this table on every request. A @Scheduled(cron) task runs nightly at 2 AM to delete rows where expiresAt < NOW().

Key considerations:

  • Storing the hash (not the raw token) means a database breach cannot be used to forge auth headers.
  • The cleanup job prevents unbounded table growth.

15 Implementation Approach

Step-by-step

  1. Generate RSA-2048 key pair (see Key Generation section). Export Base64-encoded PKCS#8 private key and X.509 public key.
  2. Configure application.yml: inject keys via environment variables JWT_PRIVATE_KEY / JWT_PUBLIC_KEY. Set access token expiry to 15 min and refresh token expiry to 7 days.
  3. Implement JwtService: decode keys in constructor. Build generateAccessToken() using Jwts.builder() with .signWith(privateKey, Jwts.SIG.RS256). Implement isTokenValid() using Jwts.parser().verifyWith(publicKey).
  4. Configure SecurityConfig: disable sessions, whitelist public endpoints, register JwtAuthFilter.
  5. Implement AuthController + AuthenticationService: validate credentials via UserDetailsService + BCrypt, generate token pair, persist refresh token hash.
  6. Implement JwkController: extract RSAPublicKey fields and serialize as JWKS JSON.
  7. Implement RevokedAccessTokenService: denyAccessToken() hashes and persists. Nightly cleanup job via @Scheduled.
  8. Write Flyway migration V1__init_schema.sql for revoked_access_tokens and refresh_tokens tables.

Key patterns used

  • Filter Chain Pattern: Spring Security filter chain for per-request token interception.
  • Strategy Pattern: SmsProvider / EmailProvider abstraction extended to auth providers.
  • Template Method: AuthenticationService orchestrates credential validation, then delegates token generation.
  • Scheduled Jobs: @Scheduled for deny-list housekeeping without an external cron dependency.

Important configurations

				
					jwt:  
  issuer: http://localhost:8091  
  private-key: ${JWT_PRIVATE_KEY}   # Base64 PKCS#8  
  public-key:  ${JWT_PUBLIC_KEY}    # Base64 X.509  
  access-token-expiry-ms: 900000    # 15 min  
  refresh-token-expiry-ms: 604800000 # 7 days  
   
spring:  
  datasource:  
    url: jdbc:postgresql://localhost:5432/reusable-components?currentSchema=auth  
  flyway:  
    enabled: true  
    schemas: auth  
				
			

16 Dependencies

Dependencies & Project Setup

To build the Authorization Service from scratch, start a new Spring Boot project via Spring Initializer and include the following dependencies. Each group covers a distinct responsibility.

1. Spring Security & Web

				
					<!-- Spring Web & REST -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
   
<!-- Spring Security -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>  
   
<!-- OAuth2 Resource Server (JWT validation & parsing support) -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>  
</dependency>
				
			

2. JJWT – Token Generation & Validation

JJWT (Java JWT) is the library used for building, signing, and parsing JWTs. All three artifacts are required: the API, the runtime implementation, and the Jackson serializer.

				
					<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-api</artifactId>  
    <version>0.12.6</version>  
</dependency>  
<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-impl</artifactId>  
    <version>0.12.6</version>  
    <scope>runtime</scope>  
</dependency>  
<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-jackson</artifactId>  
    <version>0.12.6</version>  
    <scope>runtime</scope>  
</dependency>
				
			
3. Database & Migrations
				
					<!-- JPA + Hibernate -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-jpa</artifactId>  
</dependency>  
   
<!-- PostgreSQL Driver -->  
<dependency>  
    <groupId>org.postgresql</groupId>  
    <artifactId>postgresql</artifactId>  
    <scope>runtime</scope>  
</dependency>  
   
<!-- Flyway Core -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-flyway</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.flywaydb</groupId>  
    <artifactId>flyway-database-postgresql</artifactId>  
</dependency>
				
			
4. Developer Utilities
				
					<!-- Lombok — reduces boilerplate (@Slf4j, @RequiredArgsConstructor, etc.) -->  
<dependency>  
    <groupId>org.projectlombok</groupId>  
    <artifactId>lombok</artifactId>  
    <optional>true</optional>  
</dependency>  
   
<!-- Spring Boot Validation (for @Valid on request DTOs) -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-validation</artifactId>  
</dependency>
				
			

5. Writing a Core Implementation: JwtService

Once dependencies are in place, the JwtService wires everything together. It decodes the RSA keys on startup and exposes generateAccessToken() and isTokenValid() to the rest of the service:

				
					@Slf4j  
@Service  
public class JwtService {  
   
    private final JwtProperties jwtProperties;  
    private final PrivateKey privateKey;  
    private final PublicKey publicKey;  
   
    public JwtService(JwtProperties jwtProperties) {  
        this.jwtProperties = jwtProperties;  
        try {  
            KeyFactory kf = KeyFactory.getInstance("RSA");  
            this.privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(  
                Decoders.BASE64.decode(jwtProperties.getPrivateKey())));  
            this.publicKey = kf.generatePublic(new X509EncodedKeySpec(  
                Decoders.BASE64.decode(jwtProperties.getPublicKey())));  
        } catch (Exception e) {  
            throw new IllegalStateException("Failed to parse RSA keys", e);  
        }  
    }  
   
    public String generateAccessToken(UserDetails userDetails) {  
        Date now    = new Date();  
        Date expiry = new Date(now.getTime() +  
            jwtProperties.getAccessTokenExpiryMs());  
        return Jwts.builder()  
                .subject(userDetails.getUsername())  
                .issuer(jwtProperties.getIssuer())  
                .claim("role", extractRole(userDetails))  
                .issuedAt(now).expiration(expiry)  
                .signWith(privateKey, Jwts.SIG.RS256)  
                .compact();  
    }  
   
    public boolean isTokenValid(String token) {  
        try {  
            Jwts.parser().verifyWith(publicKey).build()  
                .parseSignedClaims(token);  
            return true;  
        } catch (JwtException | IllegalArgumentException e) {  
            return false;  
        }  
    }  
}
				
			

What this does: Decodes Base64 RSA keys once at startup using Java’s KeyFactory. generateAccessToken() builds a signed JWT with role claims. isTokenValid() verifies signature and expiry in a single parser call.

Why it matters: All cryptographic operations are centralised in one class. No other service component handles raw keys or token parsing — a clean separation that makes key rotation a one-class change.

17 Code Snippets

Snippet 1: RSA Token Generation (JwtService)
				
					public String generateAccessToken(UserDetails userDetails) {  
    Date now    = new Date();  
    Date expiry = new Date(now.getTime() + jwtProperties.getAccessTokenExpiryMs());  
   
    return Jwts.builder()  
            .subject(userDetails.getUsername())  
            .issuer(jwtProperties.getIssuer())  
            .claim("role", extractRole(userDetails))  
            .issuedAt(now)  
            .expiration(expiry)  
            .signWith(privateKey, Jwts.SIG.RS256)  
            .compact();  
}  
				
			

What this does: Constructs a signed JWT containing the username, issuer, role claim, and expiry. Signs it with the RS256 private key.

Why it matters: The private key never leaves this service. Any consumer with the public key can independently verify this token without a network call.

Snippet 2: Stateless Security Filter Chain

				
					http  
    .csrf(AbstractHttpConfigurer::disable)  
    .authorizeHttpRequests(auth -> auth  
        .requestMatchers(Constants.PUBLIC_ENDPOINTS).permitAll()  
        .anyRequest().authenticated())  
    .sessionManagement(s ->  
        s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    .addFilterBefore(jwtAuthFilter,  
        UsernamePasswordAuthenticationFilter.class);  
				
			

What this does: Disables sessions entirely, whitelists public routes, and inserts the JWT validation filter before Spring’s default auth filter.

Why it matters: STATELESS policy ensures Spring never creates an HttpSession, guaranteeing true statelessness across all replicas.

Snippet 3: SHA-256 Token Deny-List

				
					@Transactional  
public void denyAccessToken(String rawToken, Instant expiresAt) {  
    RevokedAccessToken record = RevokedAccessToken.builder()  
            .tokenHash(jwtService.hashToken(rawToken))  // SHA-256 only  
            .expiresAt(expiresAt)  
            .build();  
    repository.save(record);  
}  
   
@Scheduled(cron = "0 0 2 * * *")  
@Transactional  
public void cleanupExpiredTokens() {  
    repository.deleteExpiredTokens(Instant.now());  
}
				
			

What this does: Persists only the SHA-256 hash of the token on logout. A nightly job at 2 AM deletes expired rows.

Why it matters: Raw tokens are never stored. Even a full DB dump cannot be used to replay logged-out tokens.

Full Working Code

GitHub Repo: https://github.com/your-org/authorization-service All source files, Flyway migrations, Docker Compose, and integration tests are available in the repository.

18 Key Generation

Generating the RSA Key Pair

Option 1: OpenSSL (Recommended)

				
					# 1. Generate 2048-bit RSA private key  
openssl genrsa -out private_key.pem 2048  
   
# 2. Convert to PKCS#8 (Java KeyFactory format)  
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \  
    -in private_key.pem -out private_key_pkcs8.pem  
   
# 3. Extract X.509 public key  
openssl rsa -in private_key.pem -pubout -out public_key.pem  
   
# 4. Strip headers — get raw Base64 for application.yml  
grep -v 'PRIVATE KEY' private_key_pkcs8.pem | tr -d '\n'   # -> JWT_PRIVATE_KEY  
grep -v 'PUBLIC KEY'  public_key.pem        | tr -d '\n'   # -> JWT_PUBLIC_KEY
				
			
Option 2: Java Keytool
				
					keytool -genkeypair -alias auth-service \  
        -keyalg RSA -keysize 2048 -keystore auth.jks \  
        -storepass changeit -validity 3650
        
// Extract keys programmatically:  
KeyStore ks = KeyStore.getInstance("JKS");  
ks.load(new FileInputStream("auth.jks"), "changeit".toCharArray());  
PrivateKey priv = (PrivateKey) ks.getKey("auth-service", "changeit".toCharArray());  
PublicKey  pub  = ks.getCertificate("auth-service").getPublicKey();  
System.out.println(Base64.getEncoder().encodeToString(priv.getEncoded()));  
System.out.println(Base64.getEncoder().encodeToString(pub.getEncoded()));  
				
			

Option 3: Programmatic (Dev Only)

				
					KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");  
gen.initialize(2048);  
KeyPair pair = gen.generateKeyPair();  
// Log Base64 strings — paste into application.yml  
log.info("Private: {}", Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()));  
log.info("Public:  {}", Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()));
				
			

Warning — Dev Only: Programmatic in-memory keys change on every restart. All previously issued tokens will be invalid immediately. Use static, externally managed keys in staging and production.

19 END-to-END Flow

Login Flow
				
					INPUT:   POST /api/v1/auth/login  
    { "username": "alice@example.com", "password": "s3cr3t" }  
   
PROCESSING:  
    1. AuthenticationManager.authenticate() — BCrypt credential check  
    2. JwtService.generateAccessToken()  — RS256 signed, 15 min TTL  
    3. JwtService.generateRefreshToken() — opaque UUID, 7 day TTL  
    4. Refresh token hash stored in DB  
   
OUTPUT:  200 OK  
{  
    "accessToken":  "eyJhbGciOiJSUzI1NiJ9...",  
    "refreshToken": "d4f2a8c1-...",  
    "expiresIn":    900  
}
				
			
Protected Request Flow
				
					INPUT: GET /api/v1/profile  
    Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...  
   
PROCESSING:  
    1. JwtAuthFilter extracts Bearer token  
    2. JwtService.isTokenValid() — verifies RS256 signature + expiry  
    3. RevokedTokenRepository.existsByHash(sha256(token)) — deny-list check  
    4. SecurityContext populated with UserDetails + authorities  
   
OUTPUT:  200 OK  — request reaches controller  
    401 Unauthorized — if token invalid, expired, or revoked 
				
			
Logout Flow
				
					INPUT: POST /api/v1/auth/logout  
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
   
PROCESSING:  
    1. Extract raw token from Authorization header
    2. RevokedAccessTokenService.denyAccessToken(token, expiresAt)
     -> SHA-256(token) stored in revoked_access_tokens
    3. All refresh tokens for the user marked as revoked 
   
OUTPUT:  200 OK  { "message": "Logged out successfully" }
				
			

20 Best Practices

Security considerations

  • Never commit raw private keys to source control. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets).
  • Keep access token TTL short (15 minutes or less). A shorter window limits the impact of a compromised token.
  • Hash revoked tokens (SHA-256) — never store raw token strings in any persistence layer.
  • Use BCryptPasswordEncoder with a work factor of at least 10 for password hashing.
  • Return generic 401 responses. Never hint whether a username exists or a password was wrong.

Performance considerations

  • Cache the JWKS public key in downstream services (1-hour TTL). Only re-fetch on kid mismatch.
  • Index revoked_access_tokens.token_hash for O(1) deny-list lookups.
  • The nightly cleanup job prevents the deny-list table from growing indefinitely.
  • Access token verification is entirely in-memory (no DB call) for valid, non-revoked tokens.

Scalability considerations

  • The auth service is stateless and horizontally scalable — all states are in the DB or the JWT itself.
  • Multiple instances share the same RSA key pair via environment variables — no key sync problem.
  • The deny-list DB is the only shared state. Use connection pooling (HikariCP) and read replicas if needed.

21 Common Mistakes

  • Using HS256 Across Multiple Services: Increases risk and expands secret distribution.
  • Calling Auth Service for Every Request: Creates unnecessary coupling and latency.
  • Long-Lived Access Tokens: Increases exposure if tokens are compromised.
  • Ignoring Key Rotation: Creates long-term security risks.
  • Storing Private Keys in Source Control: A surprisingly common and dangerous mistake.

22 Limitations/Trade-Offs

What this approach does NOT solve

  • Real-time revocation at scale: the deny-list DB is checked per request. Under extreme write load (mass logout events), this can become a bottleneck. Consider Redis for the deny-list in high-throughput scenarios.
  • Role changes mid-session: if a user’s roles change, their existing access token remains valid until expiry. Force a re-login or use a very short TTL to mitigate.
  • Token theft: RS256 does not prevent token theft. Use HTTPS everywhere and consider DPoP (Demonstrating Proof of Possession) for extra binding.

When not to use it

  • Simple single-service applications: if you have one service, a session cookie is simpler and more secure (SameSite, HttpOnly).
  • When you need sub-second revocation globally: JWT revocation latency equals the access token TTL. For financial or high-security contexts, consider opaque tokens with introspection.
  • Regulated environments requiring server-side session audit: stateless tokens make server-side session listing impossible.

23 Extensions/Future Enhancements

Possible improvements

  • Redis-backed deny-list: Replace PostgreSQL deny-list with Redis for sub-millisecond revocation lookups and built-in TTL expiry (no cleanup job needed).
  • Key rotation: Support multiple active key IDs (kid) in the JWKS endpoint. Rotate the signing key on a schedule without downtime.
  • MFA / OTP support: Add a pre-authentication step for TOTP or email OTP before issuing tokens.
  • OAuth2 Authorization Server: Migrate to Spring Authorization Server for full OIDC compliance including authorization_code flow and PKCE.
  • Audit logging: Emit login, logout, and token refresh events to a Kafka topic for security analytics.

Scaling ideas

  • Deploy the auth service behind a dedicated load balancer with connection draining to handle rolling key rotation.
  • Use a CDN or edge cache for the JWKS endpoint — it changes infrequently but is read by every service instance.
  • Partition the deny-list table by created_at month to keep cleanup queries on small, recent partitions.

24 Production Considerations

Credential Storage

Passwords should never be stored in plain text. Use adaptive hashing algorithms such as:

  • BCrypt
  • Argon2

Brute Force Protection

Authentication endpoints are common attack targets. Implement:

  • Rate limiting
  • Temporary lockouts
  • Monitoring

Audit Logging

Track:

  • Login attempts
  • Failed authentications
  • Token issuance
  • Token revocation

Key Management

Private keys should be stored securely. Examples:

  • AWS KMS
  • Azure Key Vault
  • HashiCorp Vault

Avoid storing signing keys directly within application repositories.

Monitoring

Track:

  • Authentication failures
  • Token issuance volume
  • JWKS availability
  • Login latency

Authentication failures often provide early indicators of security incidents.

Conclusion

Authentication is fundamentally a trust problem.

In distributed systems, solving that problem requires more than validating usernames and passwords. It requires a scalable mechanism for establishing, distributing, and verifying trust across multiple independently deployed services.

By combining JWT authentication, asymmetric cryptography, JWKS-based key distribution, and local token validation, the Authorization Service provides a secure and scalable foundation for platform-wide authentication.

Most importantly, the architecture allows business services to remain focused on business capabilities while authentication concerns remain centralized and manageable.

As platforms grow, this separation becomes increasingly valuable, enabling security infrastructure to evolve independently while maintaining consistent trust across the ecosystem.

References

Resource Link / Description
GitHub Repository https://github.com/your-org/authorization-service — Full source code, Flyway migrations, Docker Compose.
Spring Security Docs https://docs.spring.io/spring-security/reference — Official Spring Security reference.
JWT Library https://github.com/jwtk/jjwt — JWT library used for token generation and parsing.
RFC 7519 — JWT https://datatracker.ietf.org/doc/html/rfc7519 — JWT specification.
RFC 7517 — JWKS https://datatracker.ietf.org/doc/html/rfc7517 — JSON Web Key Set specification.
Spring Auth Server https://spring.io/projects/spring-authorization-server — Full OIDC authorization-server for compliance.

Business Impact

Efficiency improvements

  • Eliminated per-request auth service calls: downstream services verify tokens in memory (~0 ms) vs. a network introspection call (~5–20 ms). At 1,000 RPS, this saves thousands of synchronous network hops per second.
  • Single auth service manages identity for all microservices — no duplicated authentication logic across teams.

Cost reduction

  • No session store infrastructure (Redis cluster or DB table for sessions) is required for the auth layer itself.
  • Horizontal scaling of downstream services is simpler — no sticky sessions or shared session storage is needed.

KPI impact

KPIImpact
Auth latency (P99)Reduced from ~18 ms (introspection) to <1 ms (local JWT verify) for downstream services.
Auth service availabilitySingle point of failure risk eliminated for downstream services — they verify independently.
Developer onboardingNew services integrate by fetching JWKS once. No shared secret coordination is required.
Security posturePrivate key never leaves auth service. No secret rotation coordination across services.

About

Built by [Your Organization / Team Name]

Platform Engineering • Security Infrastructure • Backend Services

Questions or contributions? Open an issue on GitHub or reach out to the platform team.

Source Code: https://github.com/inexture-solutions/springboot-microservices/tree/main/auth-service

FAQs

Why separate Auth Service and User Service?

Authentication and user lifecycle management evolve independently and benefit from separate ownership boundaries.

Why use RS256 instead of HS256?

RS256 allows services to validate tokens without possessing signing credentials.

Does every request require a call to the Auth Service?
No. Services validate tokens locally using the public key.

What happens if the Auth Service goes down?

Already issued tokens continue working until expiration because validation occurs locally.

Is JWT always the right choice?
Not necessarily. Smaller systems may prefer session-based authentication, while distributed systems generally benefit from stateless tokens.

How often should signing keys rotate?
This depends on organizational security requirements, but periodic rotation should be considered standard practice.

Bharat Chaudhari is a Java Backend Engineer with 5+ years of experience building scalable microservices using Spring Boot. He specializes in distributed systems, event-driven architecture, Spring Security, Kafka, PostgreSQL, Redis, Docker, and Kubernetes, with a passion for building production-ready backend platforms.

Delivering Engineering Excellence Across Global Markets

  • India
  • USA
  • UAE
  • Europe
  • Singapore
  • Australia

Need Help Building Enterprise Spring Boot Microservices?

We specialize in designing secure, scalable, and event-driven microservices using Spring Boot, Kafka, PostgreSQL, Redis, OAuth2/JWT, Docker, Kubernetes, and cloud-native best practices for enterprise applications.