0Pricing
Spring Boot 4 Complete Guide · Lesson

Opaque Token Introspection and Token Exchange

Validate opaque tokens through introspection and propagate identity with token exchange.

Opaque Token Introspection and Token Exchange is a free Spring Boot 4 Complete Guide lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Spring Boot 4 Complete Guide learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Opaque Tokens Need Introspection

A JWT carries its claims inside the token, so a resource server can validate it offline by checking the signature. An opaque token is just a random reference string — it contains no claims and no signature you can verify.

  • To learn who the token belongs to and whether it is still valid, the resource server must ask the authorization server.
  • That call is the OAuth2 Token Introspection endpoint, defined by RFC 7662.
  • The authorization server replies with active: true/false plus claims like sub, scope, and exp.

Opaque tokens trade offline speed for instant revocation: revoke at the AS and the very next introspection returns active: false.

The Introspection Response

Per RFC 7662, the introspection endpoint accepts the token as a form parameter and returns a JSON document. The single mandatory field is active.

A typical successful response looks like the JSON below. If the token is unknown, expired, or revoked, the server returns simply {"active": false} and Spring rejects the request.

{
  "active": true,
  "sub": "user-123",
  "scope": "orders:read orders:write",
  "client_id": "web-app",
  "username": "alice",
  "token_type": "Bearer",
  "exp": 1735689600,
  "iat": 1735686000
}

Configuring Opaque Token Validation

In a Spring Boot 4 resource server you enable opaque-token validation with a single properties block. Spring Security autoconfigures an OpaqueTokenIntrospector from these values.

  • introspection-uri — the AS endpoint that implements RFC 7662.
  • client-id / client-secret — the resource server's own credentials to authenticate the introspection call.
# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://auth.example.com/oauth2/introspect
          client-id: resource-server
          client-secret: ${INTROSPECT_SECRET}

Enabling It in the Security Filter Chain

The properties only take effect once you opt into opaqueToken() on the resource server DSL. This wires the autoconfigured introspector into the bearer-token filter.

Notice the contrast with JWT mode: with opaque tokens there is no local key set — every request triggers a network call to the AS.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll())
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(Customizer.withDefaults()));
        return http.build();
    }
}

A Custom OpaqueTokenIntrospector

You can define your own OpaqueTokenIntrospector bean to control the HTTP client, caching, or response parsing. The built-in SpringOpaqueTokenIntrospector uses a RestClient under the hood.

Returning a bean overrides the autoconfigured one, so you keep the same opaqueToken() DSL while customizing behavior.

@Bean
OpaqueTokenIntrospector introspector(
        @Value("${spring.security.oauth2.resourceserver.opaque-token.introspection-uri}") String uri,
        @Value("${spring.security.oauth2.resourceserver.opaque-token.client-id}") String clientId,
        @Value("${spring.security.oauth2.resourceserver.opaque-token.client-secret}") String secret) {

    RestClient restClient = RestClient.builder()
        .requestInterceptor(new BasicAuthenticationInterceptor(clientId, secret))
        .build();

    return new SpringOpaqueTokenIntrospector(uri, restClient);
}

Mapping Scopes to Authorities

By default the introspected scope string becomes a set of SCOPE_ authorities. To customize that mapping — for example adding role authorities — wrap the default introspector and transform its principal.

The OAuth2AuthenticatedPrincipal exposes the raw introspection claims via getAttributes(), which you remap into your own authorities.

public class CustomAuthoritiesIntrospector implements OpaqueTokenIntrospector {

    private final OpaqueTokenIntrospector delegate;

    public CustomAuthoritiesIntrospector(OpaqueTokenIntrospector delegate) {
        this.delegate = delegate;
    }

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
        List<GrantedAuthority> authorities = new ArrayList<>(principal.getAuthorities());
        String scope = principal.getAttribute("scope");
        if (scope != null && scope.contains("orders:write")) {
            authorities.add(new SimpleGrantedAuthority("ROLE_ORDER_MANAGER"));
        }
        return new DefaultOAuth2AuthenticatedPrincipal(
            principal.getName(), principal.getAttributes(), authorities);
    }
}

Caching Introspection Results

Because every request hits the AS, opaque tokens can become a latency and load bottleneck. A safe optimization is to cache the introspection result for a short window — never longer than the token's remaining lifetime.

  • Cache on the token value as the key.
  • Honor exp: an entry must expire at or before the token's own expiry.
  • Keep the TTL small (seconds to a minute) so revocation stays nearly immediate.

This small helper shows the core TTL math you would apply inside a caching introspector.

import java.time.Instant;

public class IntrospectionCache {

    public static long ttlSeconds(long tokenExpEpoch, long maxCacheSeconds) {
        long now = Instant.now().getEpochSecond();
        long remaining = tokenExpEpoch - now;
        if (remaining <= 0) {
            return 0; // already expired, do not cache
        }
        return Math.min(remaining, maxCacheSeconds);
    }

    public static void main(String[] args) {
        long exp = Instant.now().getEpochSecond() + 300; // expires in 5 min
        System.out.println("Cache for " + ttlSeconds(exp, 30) + "s");
        System.out.println("Cache for " + ttlSeconds(Instant.now().getEpochSecond() - 10, 30) + "s");
    }
}

The Identity Propagation Problem

Now suppose your API gateway received a token for the user, and it must call a downstream service. Forwarding the original token leaks audience and over-grants scope. Re-using the gateway's own client credentials loses the user's identity entirely.

OAuth2 Token Exchange (RFC 8693) solves this: the gateway presents the incoming token to the AS and asks for a new token scoped to the downstream service, while preserving the original subject.

  • Identity is propagated, not impersonated blindly.
  • The new token's aud targets exactly the downstream service.
  • Scopes can be narrowed for least privilege.

The Token Exchange Request

A token exchange is a POST to the AS token endpoint with grant_type=urn:ietf:params:oauth:grant-type:token-exchange. The key parameters are the subject_token (the incoming token) and its subject_token_type.

  • subject_token — the token representing the user.
  • requested_token_type — usually an access token.
  • audience or resource — the downstream service the new token is for.
POST /oauth2/token HTTP/1.1
Host: auth.example.com
Authorization: Basic <gateway-client-credentials>
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<incoming-user-token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&requested_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=orders-service

Token Exchange in Spring Security 6.3+

Spring Security ships a TokenExchangeOAuth2AuthorizedClientProvider so a resource server can exchange the current token before calling downstream. Register the provider and a matching client registration with the token-exchange grant type.

The client registration uses authorization-grant-type set to the RFC 8693 URN.

@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository registrations,
        OAuth2AuthorizedClientRepository clients) {

    OAuth2AuthorizedClientProvider provider =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .provider(new TokenExchangeOAuth2AuthorizedClientProvider())
            .build();

    DefaultOAuth2AuthorizedClientManager manager =
        new DefaultOAuth2AuthorizedClientManager(registrations, clients);
    manager.setAuthorizedClientProvider(provider);
    return manager;
}

Calling Downstream With the Exchanged Token

With the manager in place, configure a RestClient (or WebClient) that attaches the exchanged token automatically. The interceptor resolves the authorized client named orders-service, triggering the exchange when needed.

The downstream service then validates a token whose sub is still the original user and whose aud is itself — clean, least-privilege identity propagation.

@Bean
RestClient ordersRestClient(OAuth2AuthorizedClientManager manager) {
    OAuth2ClientHttpRequestInterceptor interceptor =
        new OAuth2ClientHttpRequestInterceptor(manager);
    interceptor.setClientRegistrationIdResolver(request -> "orders-service");

    return RestClient.builder()
        .baseUrl("https://orders.internal")
        .requestInterceptor(interceptor)
        .build();
}

Quick Check: Choosing the Right Mechanism

Test your understanding of when to use introspection versus token exchange.

Recap

You learned how to validate reference tokens and propagate identity across services:

  • Opaque tokens carry no claims, so the resource server calls the AS introspection endpoint (RFC 7662); the only required field is active.
  • Enable it with the opaque-token properties plus oauth2ResourceServer(o -> o.opaqueToken(...)); customize via a custom OpaqueTokenIntrospector.
  • Map scopes to authorities and cache results briefly — never beyond the token's exp — to limit AS load while keeping revocation fast.
  • Token exchange (RFC 8693) swaps an incoming token for a downstream-scoped one, preserving the original sub while setting the correct aud.
  • In Spring, the TokenExchangeOAuth2AuthorizedClientProvider plus an authorized-client interceptor makes this automatic for downstream calls.

Frequently asked questions

Is the “Opaque Token Introspection and Token Exchange” lesson free?

Yes — the full text of “Opaque Token Introspection and Token Exchange” is free to read here on the web, and the Spring Boot 4 Complete Guide course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Spring Boot 4 Complete Guide course, upgrade to CoddyKit PRO.

What will I learn in “Opaque Token Introspection and Token Exchange”?

Validate opaque tokens through introspection and propagate identity with token exchange. You practise Spring Boot 4 Complete Guide with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Spring Boot 4 Complete Guide?

No prior experience is required. Spring Boot 4 Complete Guide on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Opaque Token Introspection and Token Exchange” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Spring Boot 4 Complete Guide lesson?

Yes. Every Spring Boot 4 Complete Guide lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Resource Server JWT Validation and Claims
  2. OAuth2 Client and Authorization Code Flow
  3. Method Security with SpEL and Custom Voters
  4. Opaque Token Introspection and Token Exchange
← Back to Spring Boot 4 Complete Guide