skip to content

What is the OpaqueTokenIntrospector interface, and how does Spring use it to turn an opaque token into an authenticated principal?

level: middleimportance: must knowfreq 55%

answer

  1. One method: introspect(token) -> OAuth2AuthenticatedPrincipal
  2. SpringOpaqueTokenIntrospector (Nimbus) default
  3. scope -> SCOPE_ authorities
  4. OpaqueTokenAuthenticationProvider -> BearerTokenAuthentication
  5. Custom @Bean = override + remap/cache

basics

~10 s

OpaqueTokenIntrospector has one method, introspect(token), that calls the introspection endpoint and returns an OAuth2AuthenticatedPrincipal with the token's attributes and authorities. Spring's OpaqueTokenAuthenticationProvider calls it and builds a BearerTokenAuthentication.

solid answer

~40 s

OpaqueTokenIntrospector is a functional interface with a single method, OAuth2AuthenticatedPrincipal introspect(String token). Its job is to take the raw opaque token, perform the RFC 7662 introspection call, and return a principal containing the token's attributes and granted authorities — or throw a BadOpaqueTokenException / OAuth2IntrospectionException on an inactive token or failure. Spring auto-configures SpringOpaqueTokenIntrospector (Nimbus-based), which POSTs to introspection-uri with the resource server's client credentials, checks active:true, and maps the scope claim to SCOPE_-prefixed authorities. At request time BearerTokenAuthenticationFilter extracts the header, OpaqueTokenAuthenticationProvider invokes the introspector, and the returned principal is wrapped in a BearerTokenAuthentication placed in the SecurityContext. You can override behavior by declaring your own @Bean OpaqueTokenIntrospector — commonly to remap authorities or add caching.

code

java · 16 lines
java
@Bean
OpaqueTokenIntrospector customIntrospector(OAuth2ResourceServerProperties props) {
    OAuth2ResourceServerProperties.Opaquetoken p = props.getOpaquetoken();
    SpringOpaqueTokenIntrospector delegate = new SpringOpaqueTokenIntrospector(
            p.getIntrospectionUri(), p.getClientId(), p.getClientSecret());

    return token -> {
        OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
        Collection<GrantedAuthority> authorities = ((List<String>)
                principal.getAttribute("roles")).stream()
                .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
                .collect(Collectors.toList());
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), authorities);
    };
}

go deeper

for a junior

Know that some component calls the endpoint and produces the logged-in principal.

for a middle

Should name the interface's single method, the default SpringOpaqueTokenIntrospector, scope->SCOPE_ mapping, and the provider->BearerTokenAuthentication flow.

for a senior

Should know that a custom bean replaces auto-config and is the hook for authority remapping and caching, plus the exception-to-401 mapping.

for a principal

Weighs where authority mapping and caching should live and the operational impact of per-request introspect calls.

## The interface ```java @FunctionalInterface public interface OpaqueTokenIntrospector { OAuth2AuthenticatedPrincipal introspect(String token); } ``` It has **one responsibility**: given the raw opaque token string, return an **`OAuth2AuthenticatedPrincipal`** describing it, or throw if the token is not valid. - **`OAuth2AuthenticatedPrincipal`** exposes `getName()`, `getAttributes()` (the introspection response fields — `sub`, `scope`, `exp`, `client_id`, custom claims…), and `getAuthorities()`. - On an **inactive/unknown** token the default implementation throws **`BadOpaqueTokenException`** (a subclass of `OAuth2IntrospectionException`); transport/parse problems throw `OAuth2IntrospectionException`. Both map to **401**. ## Default implementation With `spring.security.oauth2.resourceserver.opaquetoken.*` set, Boot creates a **`SpringOpaqueTokenIntrospector`** (older name: `NimbusOpaqueTokenIntrospector`). It: 1. Builds an HTTP `POST` to `introspection-uri` with body `token=<token>`. 2. Authenticates with `client-id`/`client-secret` (HTTP Basic by default). 3. Parses the JSON; if `active` is not `true`, throws `BadOpaqueTokenException`. 4. Copies response fields into the principal's attributes. 5. Maps the space-delimited **`scope`** claim to `GrantedAuthority`s prefixed **`SCOPE_`** (so `@PreAuthorize("hasAuthority('SCOPE_read')")` or `hasRole` semantics via scope work). ## Runtime flow 1. **`BearerTokenAuthenticationFilter`** pulls `Bearer <token>` from the `Authorization` header into a `BearerTokenAuthenticationToken`. 2. The **`AuthenticationManager`** delegates to **`OpaqueTokenAuthenticationProvider`**, which calls `introspector.introspect(token)`. 3. The returned principal is wrapped in a **`BearerTokenAuthentication`** (an `AbstractOAuth2TokenAuthenticationToken`) and stored in the `SecurityContext`. 4. Authorization rules then run against its authorities. ## Customizing Declaring your own bean **replaces** the auto-configured one: ```java @Bean OpaqueTokenIntrospector introspector(OAuth2ResourceServerProperties props) { var p = props.getOpaquetoken(); var delegate = new SpringOpaqueTokenIntrospector( p.getIntrospectionUri(), p.getClientId(), p.getClientSecret()); return token -> { OAuth2AuthenticatedPrincipal principal = delegate.introspect(token); // remap 'authorities' or 'roles' claim to GrantedAuthority list var mapped = extractAuthorities(principal); return new DefaultOAuth2AuthenticatedPrincipal( principal.getName(), principal.getAttributes(), mapped); }; } ``` This is the standard hook for **authority remapping** (e.g. a `roles` claim rather than `scope`) or wrapping the delegate with a **cache** to avoid an introspection round-trip per request. ## Gotchas - Declaring the bean is an **all-or-nothing override** — you lose the property-driven auto-config unless you rebuild the delegate from the properties as above. - Don't swallow `OAuth2IntrospectionException` into a generic error — that would turn a genuine 401 into a 500. - `introspect` is called **on every request** by default; without caching it is your latency and load bottleneck.

  • What authorities does the default introspector grant, and from which claim?
    It reads the space-delimited scope claim from the introspection response and grants one GrantedAuthority per scope, each prefixed SCOPE_ (e.g. scope 'read write' -> SCOPE_read, SCOPE_write).
  • If you declare your own OpaqueTokenIntrospector bean, do you still get the property-based configuration?
    No. Defining the bean backs off the auto-configuration entirely, so you must construct the delegate (or HTTP client) yourself, typically by injecting OAuth2ResourceServerProperties and passing introspection-uri/client-id/client-secret.

saying these in an interview costs you the question

  • Thinking introspect() returns a JWT or decodes the token locally
  • Believing a custom introspector bean coexists with the auto-configured one instead of replacing it
  • Assuming authorities come from a 'roles' claim by default (default is scope -> SCOPE_)

context