spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_token

invalid_token

Error message

Invalid bearer token

What it means

BearerTokenAuthenticationFilter throws this when authentication succeeds but the resulting access token is DPoP-bound while the request used it as a plain Bearer token. Spring Security rejects this downgrade to prevent a token issued for DPoP (proof-of-possession) usage from being replayed without the bound key proof. The error surfaces as OAuth2AuthenticationException with code invalid_token even though the token itself may be valid.

Source

Thrown at oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java:187

			this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid);
			this.authenticationEntryPoint.commence(request, response, invalid);
			return;
		}

		if (authenticationRequest == null) {
			this.logger.trace("Did not process request since did not find bearer token");
			filterChain.doFilter(request, response);
			return;
		}

		try {
			AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
			Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
			if (isDPoPBoundAccessToken(authenticationResult)) {
				// Prevent downgraded usage of DPoP-bound access tokens,
				// by rejecting a DPoP-bound access token received as a bearer token.
				BearerTokenError error = BearerTokenErrors.invalidToken("Invalid bearer token");
				throw new OAuth2AuthenticationException(error);
			}
			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
			if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
				authenticationResult = authenticationResult.toBuilder().authorities((a) -> {
					Set<String> newAuthorities = a.stream()
						.map(GrantedAuthority::getAuthority)
						.collect(Collectors.toUnmodifiableSet());
					for (GrantedAuthority currentAuthority : current.getAuthorities()) {
						if (!newAuthorities.contains(currentAuthority.getAuthority())) {
							a.add(currentAuthority);
						}
					}
				}).build();
			}
			SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
			context.setAuthentication(authenticationResult);
			this.securityContextHolderStrategy.setContext(context);
			this.securityContextRepository.saveContext(context, request, response);

View on GitHub (pinned to 96852e8860)

Solutions

  1. Update the client to send a valid DPoP proof header (DPoP) with each request and use the 'DPoP' authorization scheme
  2. Request a non-DPoP-bound (plain bearer) token from the authorization server for bearer-only clients
  3. If you must accept DPoP tokens as bearer tokens (not recommended), customize the filter/authentication logic, understanding the security implications
  4. Verify the authorization server client configuration (token_endpoint_auth binding) matches how the token will be used

Example fix

// before
request.header("Authorization", "Bearer " + accessToken);
// after (client with DPoP support)
String proof = dPoPSigner.generateProof(accessToken, httpMethod, url);
request.header("Authorization", "DPoP " + accessToken);
request.header("DPoP", proof);
Defensive patterns

Strategy: validation

Validate before calling

if (tokenEndpointResponse.getAccessToken().getTokenType().getValue().equalsIgnoreCase("DPoP")
        && !clientSupportsDPoP) {
    throw new IllegalStateException("Client received a DPoP-bound token but cannot send DPoP proofs");
}

Type guard

boolean isDpopBound(OAuth2AccessToken token) {
    return "DPoP".equalsIgnoreCase(token.getTokenType().getValue());
}

Try / catch

try {
    return chain.filter(exchange);
} catch (OAuth2AuthenticationException e) {
    if ("invalid_token".equals(e.getError().getErrorCode())) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
    }
    return Mono.error(e);
}

Prevention

When it happens

Trigger: A client obtains an access token bound to a DPoP key from the authorization server, then calls the resource server with 'Authorization: Bearer <token>' and no DPoP proof header. The filter detects the DPoP binding in the authenticated result (isDPoPBoundAccessToken) and rejects the request.

Common situations: Clients that previously used opaque/JWT bearer tokens switching to DPoP-issued tokens without updating their HTTP client; SDKs that don't support DPoP proof generation; token endpoint misconfiguration returning DPoP-bound tokens to bearer-only clients.

Related errors


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