What is the OpaqueTokenIntrospector interface, and how does Spring use it to turn an opaque token into an authenticated principal?
answer
- One method: introspect(token) -> OAuth2AuthenticatedPrincipal
- SpringOpaqueTokenIntrospector (Nimbus) default
- scope -> SCOPE_ authorities
- OpaqueTokenAuthenticationProvider -> BearerTokenAuthentication
- Custom @Bean = override + remap/cache
basics
~10 sOpaqueTokenIntrospector 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 sOpaqueTokenIntrospector 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@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
Know that some component calls the endpoint and produces the logged-in principal.
Should name the interface's single method, the default SpringOpaqueTokenIntrospector, scope->SCOPE_ mapping, and the provider->BearerTokenAuthentication flow.
Should know that a custom bean replaces auto-config and is the hook for authority remapping and caching, plus the exception-to-401 mapping.
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_)