spring-projects/spring-security · error · BadOpaqueTokenException

Provided token isn't active

Error message

Provided token isn't active

What it means

SpringOpaqueTokenIntrospector (formerly NimbusOpaqueTokenIntrospector) adapts the Nimbus introspection response and requires the 'active' claim to be true; otherwise it throws BadOpaqueTokenException('Provided token isn't active'). RFC 7662 mandates that inactive tokens carry active=false, and Spring Security treats this as authentication failure.

Solutions

  1. Inspect the raw introspection response (curl the /introspect endpoint) to confirm active and related claims
  2. Obtain a new access token
  3. Validate issuer/environment configuration of SpringOpaqueTokenIntrospector (introspectionUri, clientId, clientSecret)
  4. Check AS logs for revocation or expiry of the token
Defensive patterns

Strategy: try-catch

Validate before calling

// verify active claim directly before authenticating
Map claims = httpGET(introspectionUri, token);
if (!Boolean.TRUE.equals(claims.get("active"))) refreshAccessToken();

Try / catch

try {
    OAuth2AuthenticatedPrincipal p = introspector.introspect(token);
} catch (BadOpaqueTokenException e) {
    log.warn("Token inactive: {}", e.getMessage());
    throw new UnauthorizedException("Token is not active", e);
}

Prevention

When it happens

Trigger: introspect() receives an introspection response with active=false/missing for the presented opaque token — expired, revoked, wrong AS, or not-yet-valid token.

Common situations: Expired or revoked tokens in cached clients; dev/prod introspection endpoint mismatch; AS-side cleanup deleting the token record; tokens issued before a rotation of signing/introspection config.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/eaa3e41a4922cefe. Report an issue: GitHub.

Appendix: source

Thrown at oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java:191

		Map<String, Object> claims = responseEntity.getBody();
		// relying solely on the authorization server to validate this token (not checking
		// 'exp', for example)
		if (claims == null) {
			return Collections.emptyMap();
		}

		boolean active = (boolean) claims.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE, (k, v) -> {
			if (v instanceof String) {
				return Boolean.parseBoolean((String) v);
			}
			if (v instanceof Boolean) {
				return v;
			}
			return false;
		});
		if (!active) {
			this.logger.trace("Did not validate token since it is inactive");
			throw new BadOpaqueTokenException("Provided token isn't active");
		}
		return claims;
	}

	private ArrayListFromStringClaimAccessor convertClaimsSet(Map<String, Object> claims) {
		Map<String, Object> converted = new LinkedHashMap<>(claims);
		converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
			if (v instanceof String) {
				return Collections.singletonList(v);
			}
			return v;
		});
		converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString());
		converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP,
				(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
		converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT,
				(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
		// RFC-7662 page 7 directs users to RFC-7519 for defining the values of these

View on GitHub (pinned to 96852e8860)