In modern distributed systems and microservices architectures, tokens are the primary mechanism for authentication and authorization. OAuth2 and OpenID Connect have become the de facto standards for token-based security. However, simply receiving a token isn't enough—services need a reliable way to validate tokens, check their status, and extract relevant claims. Token introspection provides a standardized mechanism for resource servers to query authorization servers about the current state of a token, enabling real-time validation, revocation checking, and fine-grained access control.
What is Token Introspection?
Token introspection, defined in RFC 7662, is an OAuth2 protocol extension that allows resource servers to query an authorization server about the validity and metadata of an access token. When a resource server receives a token, it can call the introspection endpoint to verify:
- Token Validity: Is the token active (not expired, not revoked)?
- Token Metadata: What scopes, client ID, username, and other claims are associated?
- Token Type: Is it an access token, refresh token, or something else?
- Expiration: When does the token expire?
- Revocation Status: Has the token been explicitly revoked?
Why Token Introspection is Essential for Java Applications
- Centralized Validation: Offload token validation logic to the authorization server
- Real-Time Revocation: Immediately detect revoked tokens without waiting for expiration
- Stateless Services: Resource servers can remain stateless while still validating tokens
- Standardized Protocol: Works across different OAuth2 providers
- Rich Claims Access: Obtain comprehensive token metadata without parsing JWT
- Security: Prevents acceptance of tampered or forged tokens
Token Introspection Flow
Client → Resource Server: Present access token
Resource Server → Authorization Server: POST /introspect (token, client credentials)
Authorization Server → Resource Server: JSON response {active: true, scope: "read write", ...}
Resource Server → Client: Grant access based on introspection result
Implementing Token Introspection in Java
1. Maven Dependencies
<dependencies> <!-- Spring Security OAuth2 Client --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>6.2.0</version> </dependency> <!-- Spring Security OAuth2 Resource Server --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> <version>3.2.0</version> </dependency> <!-- Spring Web for REST clients --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Apache HttpClient --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.3.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Lombok (optional) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Micrometer for metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> </dependencies>
2. Basic Token Introspection Client
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.springframework.util.Base64Utils;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class TokenIntrospectionClient {
private final String introspectionEndpoint;
private final String clientId;
private final String clientSecret;
private final ObjectMapper objectMapper;
private final CloseableHttpClient httpClient;
// Cache introspection results
private final Map<String, CachedIntrospection> cache = new ConcurrentHashMap<>();
public TokenIntrospectionClient(String introspectionEndpoint,
String clientId,
String clientSecret) {
this.introspectionEndpoint = introspectionEndpoint;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.objectMapper = new ObjectMapper();
this.httpClient = HttpClients.createDefault();
}
/**
* Introspect a token and return its metadata
*/
public IntrospectionResponse introspect(String token) throws Exception {
// Check cache first
CachedIntrospection cached = cache.get(token);
if (cached != null && !cached.isExpired()) {
return cached.response;
}
// Create introspection request
HttpPost httpPost = new HttpPost(introspectionEndpoint);
// Set basic authentication header
String auth = clientId + ":" + clientSecret;
byte[] encodedAuth = Base64Utils.encode(auth.getBytes(StandardCharsets.UTF_8));
String authHeader = "Basic " + new String(encodedAuth);
httpPost.setHeader("Authorization", authHeader);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
// Set request body
String body = "token=" + token + "&token_type_hint=access_token";
httpPost.setEntity(new StringEntity(body));
// Execute request
IntrospectionResponse response = httpClient.execute(httpPost, httpResponse -> {
int statusCode = httpResponse.getCode();
if (statusCode == 200) {
return objectMapper.readValue(
httpResponse.getEntity().getContent(),
IntrospectionResponse.class
);
} else {
throw new RuntimeException("Introspection failed with status: " + statusCode);
}
});
// Cache the result
cache.put(token, new CachedIntrospection(response, Instant.now()));
return response;
}
/**
* Introspection response as defined in RFC 7662
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public static class IntrospectionResponse {
@JsonProperty("active")
private boolean active;
@JsonProperty("scope")
private String scope;
@JsonProperty("client_id")
private String clientId;
@JsonProperty("username")
private String username;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("exp")
private Long expiresAt;
@JsonProperty("iat")
private Long issuedAt;
@JsonProperty("nbf")
private Long notBefore;
@JsonProperty("sub")
private String subject;
@JsonProperty("aud")
private List<String> audience;
@JsonProperty("iss")
private String issuer;
@JsonProperty("jti")
private String jwtId;
@JsonProperty("ext")
private Map<String, Object> extendedAttributes;
// Getters and setters
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; }
public String getClientId() { return clientId; }
public void setClientId(String clientId) { this.clientId = clientId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
public Long getExpiresAt() { return expiresAt; }
public void setExpiresAt(Long expiresAt) { this.expiresAt = expiresAt; }
public Long getIssuedAt() { return issuedAt; }
public void setIssuedAt(Long issuedAt) { this.issuedAt = issuedAt; }
public Long getNotBefore() { return notBefore; }
public void setNotBefore(Long notBefore) { this.notBefore = notBefore; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public List<String> getAudience() { return audience; }
public void setAudience(List<String> audience) { this.audience = audience; }
public String getIssuer() { return issuer; }
public void setIssuer(String issuer) { this.issuer = issuer; }
public String getJwtId() { return jwtId; }
public void setJwtId(String jwtId) { this.jwtId = jwtId; }
public Map<String, Object> getExtendedAttributes() { return extendedAttributes; }
public void setExtendedAttributes(Map<String, Object> extendedAttributes) {
this.extendedAttributes = extendedAttributes;
}
/**
* Check if token is expired based on exp claim
*/
public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(Instant.ofEpochSecond(expiresAt));
}
/**
* Get scopes as a list
*/
public List<String> getScopeList() {
return scope != null ? List.of(scope.split("\\s+")) : List.of();
}
@Override
public String toString() {
return "IntrospectionResponse{" +
"active=" + active +
", scope='" + scope + '\'' +
", clientId='" + clientId + '\'' +
", username='" + username + '\'' +
", subject='" + subject + '\'' +
'}';
}
}
/**
* Cached introspection result with expiration
*/
private static class CachedIntrospection {
final IntrospectionResponse response;
final Instant cachedAt;
CachedIntrospection(IntrospectionResponse response, Instant cachedAt) {
this.response = response;
this.cachedAt = cachedAt;
}
boolean isExpired() {
// Cache for 30 seconds (adjust based on your needs)
return Instant.now().isAfter(cachedAt.plusSeconds(30));
}
}
public void close() throws Exception {
httpClient.close();
}
public static void main(String[] args) throws Exception {
System.out.println("=== Token Introspection Example ===\n");
// Initialize client (use actual values in production)
TokenIntrospectionClient client = new TokenIntrospectionClient(
"https://auth.example.com/oauth2/introspect",
"resource-server-client",
"client-secret"
);
try {
// Introspect a token
String accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
IntrospectionResponse response = client.introspect(accessToken);
System.out.println("Introspection Result:");
System.out.println(" Active: " + response.isActive());
System.out.println(" Subject: " + response.getSubject());
System.out.println(" Scope: " + response.getScope());
System.out.println(" Client ID: " + response.getClientId());
System.out.println(" Username: " + response.getUsername());
System.out.println(" Expires At: " + response.getExpiresAt());
if (response.isActive()) {
System.out.println("✓ Token is valid");
// Check scopes
if (response.getScopeList().contains("read")) {
System.out.println("✓ Token has read scope");
}
} else {
System.out.println("✗ Token is invalid or expired");
}
} finally {
client.close();
}
}
}
3. Spring Boot Resource Server with Introspection
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@SpringBootApplication
public class IntrospectionResourceServer {
public static void main(String[] args) {
SpringApplication.run(IntrospectionResourceServer.class, args);
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public static class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspector(opaqueTokenIntrospector())
)
);
return http.build();
}
@Bean
public OpaqueTokenIntrospector opaqueTokenIntrospector() {
return new CustomOpaqueTokenIntrospector();
}
}
/**
* Custom Opaque Token Introspector with caching
*/
public static class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate;
private final IntrospectionCache cache;
public CustomOpaqueTokenIntrospector() {
// Spring Boot auto-configures this based on properties
this.delegate = SpringBootTokenIntrospector.create();
this.cache = new IntrospectionCache();
}
@Override
public OAuth2TokenIntrospectionClaimAccessor introspect(String token) {
// Check cache first
OAuth2TokenIntrospectionClaimAccessor cached = cache.get(token);
if (cached != null) {
return cached;
}
// Perform introspection
OAuth2TokenIntrospectionClaimAccessor result = delegate.introspect(token);
// Cache if active
if (result.isActive()) {
cache.put(token, result);
}
return result;
}
}
/**
* Simple in-memory cache for introspection results
*/
public static class IntrospectionCache {
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
public OAuth2TokenIntrospectionClaimAccessor get(String token) {
CacheEntry entry = cache.get(token);
if (entry != null && !entry.isExpired()) {
return entry.claims;
}
return null;
}
public void put(String token, OAuth2TokenIntrospectionClaimAccessor claims) {
cache.put(token, new CacheEntry(claims));
}
@Scheduled(fixedRate = 60000) // Clean up every minute
public void cleanup() {
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
private static class CacheEntry {
final OAuth2TokenIntrospectionClaimAccessor claims;
final Instant cachedAt;
CacheEntry(OAuth2TokenIntrospectionClaimAccessor claims) {
this.claims = claims;
this.cachedAt = Instant.now();
}
boolean isExpired() {
return Instant.now().isAfter(cachedAt.plusSeconds(30));
}
}
}
/**
* Spring Boot auto-configured introspector
*/
public static class SpringBootTokenIntrospector {
public static OpaqueTokenIntrospector create() {
// In a real app, Spring Boot auto-configures this
// This is a placeholder for the actual implementation
return token -> {
// This would be implemented by Spring Boot based on
// spring.security.oauth2.resourceserver.opaquetoken.* properties
throw new UnsupportedOperationException(
"Auto-configured by Spring Boot - see application.yml"
);
};
}
}
@RestController
@RequestMapping("/api")
public static class ApiController {
@GetMapping("/userinfo")
public Map<String, Object> getUserInfo(BearerTokenAuthentication authentication) {
var introspected = (OAuth2TokenIntrospectionClaimAccessor)
authentication.getToken();
return Map.of(
"subject", introspected.getSubject(),
"username", introspected.getClaimAsString("username"),
"scope", introspected.getClaimAsString("scope"),
"client_id", introspected.getClaimAsString("client_id"),
"active", introspected.isActive()
);
}
@GetMapping("/data")
@PreAuthorize("hasAuthority('SCOPE_read')")
public List<String> getData() {
return List.of("data1", "data2", "data3");
}
@PostMapping("/write")
@PreAuthorize("hasAuthority('SCOPE_write')")
public String writeData(@RequestBody String data) {
return "Written: " + data;
}
}
}
4. application.yml Configuration
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://auth.example.com/oauth2/introspect
client-id: resource-server
client-secret: ${INTROSPECTION_CLIENT_SECRET}
# Optional: configure connection pool
connect-timeout: 5000
read-timeout: 5000
# Cache configuration
cache:
introspection:
enabled: true
ttl-seconds: 30
max-size: 1000
# Metrics
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
5. Reactive Token Introspection (WebFlux)
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.cache.CacheMono;
import reactor.core.publisher.Signal;
import java.time.Duration;
@Service
public class ReactiveTokenIntrospector {
private final WebClient webClient;
private final String introspectionUri;
private final String clientId;
private final String clientSecret;
// Reactive cache
private final Map<String, IntrospectionResponse> cache = new ConcurrentHashMap<>();
public ReactiveTokenIntrospector(@Value("${oauth.introspection-uri}") String introspectionUri,
@Value("${oauth.client-id}") String clientId,
@Value("${oauth.client-secret}") String clientSecret) {
this.introspectionUri = introspectionUri;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.webClient = WebClient.builder()
.baseUrl(introspectionUri)
.defaultHeaders(headers -> {
String auth = clientId + ":" + clientSecret;
String encodedAuth = Base64Utils.encodeToString(auth.getBytes());
headers.setBasicAuth(clientId, clientSecret);
})
.build();
}
public Mono<IntrospectionResponse> introspect(String token) {
return CacheMono.lookup(
cache.asMap().compute(token, (key, oldValue) -> {
if (oldValue != null && !oldValue.isExpired()) {
return oldValue;
}
return null;
}),
token
).onCacheMissResume(() ->
webClient.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.bodyValue("token=" + token + "&token_type_hint=access_token")
.retrieve()
.bodyToMono(IntrospectionResponse.class)
.doOnNext(response -> {
if (response.isActive()) {
cache.put(token, response);
}
})
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)))
);
}
public static class IntrospectionResponse {
private boolean active;
private String scope;
private String clientId;
private String username;
private Long exp;
private Instant cachedAt;
// Getters and setters
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public boolean isExpired() {
return exp != null && Instant.now().isAfter(Instant.ofEpochSecond(exp));
}
// Cache for 30 seconds
public boolean isCacheValid() {
return cachedAt != null &&
Duration.between(cachedAt, Instant.now()).getSeconds() < 30;
}
}
}
Advanced Token Introspection Patterns
1. Introspection with Retry and Circuit Breaking
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
@Component
public class ResilientTokenIntrospector {
private final TokenIntrospectionClient delegate;
private final CircuitBreaker circuitBreaker;
private final Retry retry;
private final MeterRegistry meterRegistry;
public ResilientTokenIntrospector(TokenIntrospectionClient delegate,
MeterRegistry meterRegistry) {
this.delegate = delegate;
this.meterRegistry = meterRegistry;
// Configure circuit breaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(3)
.slidingWindowSize(10)
.build();
this.circuitBreaker = CircuitBreakerRegistry.of(circuitBreakerConfig)
.circuitBreaker("introspection");
// Configure retry
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
this.retry = Retry.of("introspection", retryConfig);
// Register metrics
circuitBreaker.getEventPublisher()
.onStateTransition(event ->
meterRegistry.counter("introspection.circuitbreaker.state",
"state", event.getStateTransition().toString())
.increment()
);
}
public IntrospectionResponse introspect(String token) {
return circuitBreaker.executeSupplier(() ->
retry.executeSupplier(() -> {
try {
IntrospectionResponse response = delegate.introspect(token);
// Record metrics
meterRegistry.counter("introspection.calls",
"active", String.valueOf(response.isActive()))
.increment();
return response;
} catch (Exception e) {
meterRegistry.counter("introspection.errors",
"type", e.getClass().getSimpleName())
.increment();
throw e;
}
})
);
}
}
2. Batch Token Introspection
@Service
public class BatchTokenIntrospector {
private final TokenIntrospectionClient client;
private final ScheduledExecutorService scheduler;
public BatchTokenIntrospector(TokenIntrospectionClient client) {
this.client = client;
this.scheduler = Executors.newScheduledThreadPool(2);
}
/**
* Introspect multiple tokens in batch (if supported by auth server)
*/
public Map<String, IntrospectionResponse> introspectBatch(List<String> tokens) {
// Some auth servers support batch introspection via custom endpoint
// This example shows how to parallelize individual calls
List<CompletableFuture<Map.Entry<String, IntrospectionResponse>>> futures =
tokens.stream()
.map(token -> CompletableFuture.supplyAsync(() -> {
try {
return Map.entry(token, client.introspect(token));
} catch (Exception e) {
return Map.entry(token, null);
}
}, scheduler))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.filter(entry -> entry.getValue() != null)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
}
/**
* Scheduled token validation
*/
@Scheduled(fixedDelay = 60000)
public void validateCachedTokens() {
// Periodically re-validate cached tokens
// Implementation depends on your caching strategy
}
}
3. Token Introspection with Claim Mapping
@Component
public class ClaimMappingIntrospector {
private final TokenIntrospectionClient client;
private final Map<String, ClaimMapper> mappers;
public ClaimMappingIntrospector(TokenIntrospectionClient client) {
this.client = client;
this.mappers = new HashMap<>();
initializeMappers();
}
private void initializeMappers() {
// Map standard OAuth2 claims to application roles
mappers.put("roles", new ClaimMapper() {
@Override
public List<String> map(IntrospectionResponse response) {
String scope = response.getScope();
if (scope != null && scope.contains("admin")) {
return List.of("ROLE_ADMIN", "ROLE_USER");
} else if (scope != null) {
return List.of("ROLE_USER");
}
return List.of();
}
});
mappers.put("permissions", new ClaimMapper() {
@Override
public List<String> map(IntrospectionResponse response) {
// Map scope to granular permissions
List<String> permissions = new ArrayList<>();
String scope = response.getScope();
if (scope != null) {
if (scope.contains("read")) permissions.add("READ");
if (scope.contains("write")) permissions.add("WRITE");
if (scope.contains("delete")) permissions.add("DELETE");
}
return permissions;
}
});
}
public EnhancedIntrospectionResponse introspectWithMapping(String token)
throws Exception {
IntrospectionResponse base = client.introspect(token);
if (!base.isActive()) {
return new EnhancedIntrospectionResponse(base, false, Map.of());
}
// Apply all mappers
Map<String, Object> enhancedClaims = new HashMap<>();
for (Map.Entry<String, ClaimMapper> entry : mappers.entrySet()) {
enhancedClaims.put(entry.getKey(), entry.getValue().map(base));
}
return new EnhancedIntrospectionResponse(base, true, enhancedClaims);
}
public static class EnhancedIntrospectionResponse {
private final IntrospectionResponse base;
private final boolean valid;
private final Map<String, Object> enhancedClaims;
public EnhancedIntrospectionResponse(IntrospectionResponse base,
boolean valid,
Map<String, Object> enhancedClaims) {
this.base = base;
this.valid = valid;
this.enhancedClaims = enhancedClaims;
}
public boolean hasRole(String role) {
List<String> roles = (List<String>) enhancedClaims.getOrDefault("roles",
List.of());
return roles.contains(role);
}
public boolean hasPermission(String permission) {
List<String> permissions = (List<String>) enhancedClaims.getOrDefault(
"permissions", List.of());
return permissions.contains(permission);
}
}
interface ClaimMapper {
Object map(IntrospectionResponse response);
}
}
Security Best Practices for Token Introspection
- Always Use HTTPS: Introspection endpoints must be called over HTTPS
- Authenticate Your Calls: Use client credentials or mutual TLS
- Cache Responsibly: Cache results to reduce load but respect expiration
- Handle Failures Gracefully: Circuit breakers prevent cascading failures
- Validate All Claims: Don't just check
active; validate scopes, audience, etc. - Monitor and Alert: Track introspection failures and latency
- Rate Limiting: Implement client-side rate limiting for introspection
Example Security Configuration:
@Configuration
public class IntrospectionSecurityConfig {
@Bean
public RestTemplate introspectionRestTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(2))
.additionalInterceptors((request, body, execution) -> {
// Add security headers
request.getHeaders().set("X-Request-ID", UUID.randomUUID().toString());
return execution.execute(request, body);
})
.build();
}
@Bean
public MeterRegistryIntrospectionListener metricsListener(MeterRegistry registry) {
return new MeterRegistryIntrospectionListener(registry);
}
public static class MeterRegistryIntrospectionListener {
private final MeterRegistry registry;
public MeterRegistryIntrospectionListener(MeterRegistry registry) {
this.registry = registry;
}
public void recordIntrospection(String token, boolean success, long duration) {
Timer.builder("introspection.duration")
.tag("success", String.valueOf(success))
.register(registry)
.record(Duration.ofMillis(duration));
Counter.builder("introspection.total")
.tag("success", String.valueOf(success))
.register(registry)
.increment();
}
}
}
Integration with Different OAuth2 Providers
public class ProviderSpecificIntrospector {
public static IntrospectionClient forProvider(Provider provider,
String clientId,
String clientSecret) {
switch (provider) {
case KEYCLOAK:
return new KeycloakIntrospector(clientId, clientSecret);
case OKTA:
return new OktaIntrospector(clientId, clientSecret);
case AUTH0:
return new Auth0Introspector(clientId, clientSecret);
case AWS_COGNITO:
return new CognitoIntrospector(clientId, clientSecret);
default:
return new StandardIntrospector(clientId, clientSecret);
}
}
public enum Provider {
KEYCLOAK, OKTA, AUTH0, AWS_COGNITO, GENERIC
}
// Provider-specific implementations handle URL formats and response differences
static class KeycloakIntrospector extends StandardIntrospector {
public KeycloakIntrospector(String clientId, String clientSecret) {
super("https://auth.example.com/realms/myrealm/protocol/openid-connect/token/introspect",
clientId, clientSecret);
}
}
static class OktaIntrospector extends StandardIntrospector {
public OktaIntrospector(String clientId, String clientSecret) {
super("https://dev-123456.okta.com/oauth2/v1/introspect",
clientId, clientSecret);
}
}
}
Conclusion
Token introspection provides Java applications with a robust, standardized mechanism for validating OAuth2 tokens in real-time. By delegating token validation to the authorization server, resource servers can remain stateless while still enforcing fine-grained access control, detecting revoked tokens immediately, and obtaining rich token metadata.
The Java ecosystem offers excellent support for token introspection through Spring Security's built-in opaque token support, as well as custom implementations using HTTP clients. By following best practices—caching responses appropriately, implementing circuit breakers, and monitoring performance—developers can build secure, resilient systems that leverage the full power of OAuth2 token introspection.
Whether you're building microservices, API gateways, or traditional web applications, token introspection should be a key component of your security architecture, providing the flexibility and security required for modern distributed systems.
Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)
Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/
Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/
Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/
Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/
Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/
Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/
Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/
Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/
Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/