What is an opaque token in a Spring Security resource server, and how do you configure the resource server to validate one?
answer
- Random string, no local claims
- RFC 7662 introspection POST
- oauth2ResourceServer().opaqueToken()
- introspection-uri + client-id/secret
- active:true -> BearerTokenAuthentication
basics
~10 sAn opaque token is a random string with no readable content. The resource server can't decode it, so it calls the authorization server's introspection endpoint to check it. You enable it with oauth2ResourceServer().opaqueToken().
solid answer
~40 sAn opaque token (also called a reference token) is an unguessable random string that carries no self-contained claims — unlike a JWT you cannot decode it locally. To validate it, the resource server must ask the authorization server whether it is active by calling the RFC 7662 token-introspection endpoint. In Spring Security you enable this on the SecurityFilterChain with http.oauth2ResourceServer(o -> o.opaqueToken(Customizer.withDefaults())). You then supply the introspection URI and client credentials via spring.security.oauth2.resourceserver.opaquetoken.* properties. Spring auto-configures a SpringOpaqueTokenIntrospector (a NimbusOpaqueTokenIntrospector under the hood) that POSTs the token to the endpoint and, on an active response, builds an authenticated BearerTokenAuthentication that populates the SecurityContext. An inactive or unknown token yields 401 Unauthorized.
code
java · 19 lines@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(Customizer.withDefaults()));
return http.build();
}
}
// application.yml
// spring.security.oauth2.resourceserver.opaquetoken:
// introspection-uri: https://auth.example.com/oauth2/introspect
// client-id: my-resource-server
// client-secret: ${INTROSPECT_SECRET}go deeper
Know the definition (random string, can't be decoded), that a network introspection call validates it, and the one-line oauth2ResourceServer().opaqueToken() config plus the three properties.
Should name RFC 7662, the active flag, SpringOpaqueTokenIntrospector auto-config, and that scopes become SCOPE_ authorities on a BearerTokenAuthentication.
Should discuss the authenticated introspection call, per-request latency, and 401 mapping paths.
Frames it as a source-of-truth / revocation trade-off against JWT for the system's auth topology.
## What an opaque token is A **bearer token** sent in the `Authorization: Bearer <token>` header can be one of two shapes: - **Self-contained (JWT):** a signed JSON payload the resource server can validate and read *locally* just by verifying the signature. - **Opaque (a.k.a. reference token):** an unguessable random string (e.g. `a1b2c3...`) that carries **no readable content**. The resource server cannot decode it or learn who the user is by looking at it. The only way to find out if it is valid and what it grants is to **ask the authorization server**. ## RFC 7662 introspection The standard mechanism for that lookup is **OAuth 2.0 Token Introspection (RFC 7662)**. The resource server sends an HTTP `POST` to the authorization server's **introspection endpoint** with `token=<the-opaque-token>`, authenticating itself with client credentials. The endpoint replies with JSON; the key field is `"active": true|false`. If active, the response also typically includes `sub`, `scope`, `exp`, `client_id`, etc. ## Spring Security configuration On your `SecurityFilterChain` bean: ```java http.oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults())); ``` Then provide the endpoint and this resource server's introspection credentials in `application.yml`: ```yaml spring: security: oauth2: resourceserver: opaquetoken: introspection-uri: https://auth.example.com/oauth2/introspect client-id: my-resource-server client-secret: ${INTROSPECT_SECRET} ``` Given these properties, Spring Boot auto-configures a bean of type **`OpaqueTokenIntrospector`** — specifically **`SpringOpaqueTokenIntrospector`** (which delegates to Nimbus). The `BearerTokenAuthenticationFilter` extracts the token, an `OpaqueTokenAuthenticationProvider` calls the introspector, and on success a **`BearerTokenAuthentication`** is placed in the `SecurityContext`. Its `getName()` is the `sub` claim and its authorities are derived from the `scope` claim (prefixed `SCOPE_`). ## Gotchas - You must supply **`introspection-uri`** *and* `client-id`/`client-secret` — introspection is an authenticated call; the resource server is itself an OAuth client to the endpoint. - `opaqueToken()` and `jwt()` are mutually distinct paths. If you configure both, Spring needs an `AuthenticationManagerResolver` to decide which to use per request — you can't silently accept either shape by default. - An **inactive** token (`active:false`), an expired token, or a network failure to the endpoint results in a **401**. - Every request incurs a **network round-trip** unless you cache — this is the main cost versus JWT. ## When to use Choose opaque tokens when you need **instant revocation** and tight central control (the authorization server is the single source of truth on every call), and you accept the per-request introspection latency.
- Why does the introspection call itself need client credentials?RFC 7662 requires the caller to authenticate so the authorization server only reveals token details to trusted resource servers. The resource server acts as an OAuth client to the introspection endpoint, sending its client-id/client-secret (typically HTTP Basic).
- What HTTP status does the client get if the token is inactive or the introspection endpoint is unreachable?401 Unauthorized. An inactive/unknown token produces an OAuth2 'invalid_token' error; an endpoint failure surfaces as an authentication error, both mapped to 401 by BearerTokenAuthenticationEntryPoint.
saying these in an interview costs you the question
- Claiming the resource server can decode an opaque token to read the subject or scopes (it cannot — it has no readable content)
- Saying introspection is anonymous / needs no credentials
- Confusing opaque-token config with jwt() decoding