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
| Component | Role |
|---|---|
| JwtService | Signs access tokens with the RSA private key; verifies with the public key. |
| SecurityFilterChain | Enforces stateless session policy; routes public vs. protected endpoints. |
| JwkController | Exposes the public key in JWKS format for downstream consumers. |
| RevokedAccessTokenService | Maintains a SHA-256 deny-list with scheduled cleanup. |
| AuthController | REST API surface: /login, /refresh, /logout. |
| Flyway + PostgreSQL | Version-controlled schema for revoked tokens and refreshed token records. |
| Distributed Tracing | Uses 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
@Scheduledcleanup. - SecurityConfig: Declares stateless session policy, public routes, and filter chain order.
- Flyway + PostgreSQL: Schema migrations for
revoked_access_tokensandrefresh_tokenstables.
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:
- Generate a new key pair.
- Publish new public key via JWKS.
- Continue supporting the previous key temporarily.
- 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.STATELESSis non-negotiable — any session creation silently breaks statelessness.- Custom
CustomAuthenticationEntryPointreturns a structured JSON401instead 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
- Generate RSA-2048 key pair (see Key Generation section). Export Base64-encoded PKCS#8 private key and X.509 public key.
- Configure
application.yml: inject keys via environment variablesJWT_PRIVATE_KEY/JWT_PUBLIC_KEY. Set access token expiry to15 minand refresh token expiry to7 days. - Implement
JwtService: decode keys in constructor. BuildgenerateAccessToken()usingJwts.builder()with.signWith(privateKey, Jwts.SIG.RS256). ImplementisTokenValid()usingJwts.parser().verifyWith(publicKey). - Configure
SecurityConfig: disable sessions, whitelist public endpoints, registerJwtAuthFilter. - Implement
AuthController+AuthenticationService: validate credentials viaUserDetailsService+BCrypt, generate token pair, persist refresh token hash. - Implement
JwkController: extractRSAPublicKeyfields and serialize as JWKS JSON. - Implement
RevokedAccessTokenService:denyAccessToken()hashes and persists. Nightly cleanup job via@Scheduled. - Write Flyway migration
V1__init_schema.sqlforrevoked_access_tokensandrefresh_tokenstables.
Key patterns used
- Filter Chain Pattern: Spring Security filter chain for per-request token interception.
- Strategy Pattern:
SmsProvider/EmailProviderabstraction extended to auth providers. - Template Method:
AuthenticationServiceorchestrates credential validation, then delegates token generation. - Scheduled Jobs:
@Scheduledfor 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
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-oauth2-resource-server
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.
io.jsonwebtoken
jjwt-api
0.12.6
io.jsonwebtoken
jjwt-impl
0.12.6
runtime
io.jsonwebtoken
jjwt-jackson
0.12.6
runtime
org.springframework.boot
spring-boot-starter-data-jpa
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-starter-flyway
org.flywaydb
flyway-database-postgresql
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-validation
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
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
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
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
}
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
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
BCryptPasswordEncoderwith a work factor of at least10for password hashing. - Return generic
401responses. 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
kidmismatch. - Index
revoked_access_tokens.token_hashfor 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_codeflow andPKCE. - 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-listtable bycreated_atmonth 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
| KPI | Impact |
|---|---|
| Auth latency (P99) | Reduced from ~18 ms (introspection) to <1 ms (local JWT verify) for downstream services. |
| Auth service availability | Single point of failure risk eliminated for downstream services — they verify independently. |
| Developer onboarding | New services integrate by fetching JWKS once. No shared secret coordination is required. |
| Security posture | Private 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.
