Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,7 +40,7 @@
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
Expand Down Expand Up @@ -300,7 +300,7 @@ BeanMetadataElement getIntrospector(Element element) {
String clientId = element.getAttribute(CLIENT_ID);
String clientSecret = element.getAttribute(CLIENT_SECRET);
BeanDefinitionBuilder introspectorBuilder = BeanDefinitionBuilder
.rootBeanDefinition(NimbusOpaqueTokenIntrospector.class);
.rootBeanDefinition(SpringOpaqueTokenIntrospector.class);
introspectorBuilder.addConstructorArgValue(introspectionUri);
introspectorBuilder.addConstructorArgValue(clientId);
introspectorBuilder.addConstructorArgValue(clientSecret);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -61,6 +61,7 @@
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.ConfigurableEnvironment;
Expand Down Expand Up @@ -120,9 +121,9 @@
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
Expand All @@ -146,6 +147,7 @@
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

Expand Down Expand Up @@ -618,8 +620,8 @@ public void requestWhenDefaultConfiguredThenSessionIsNotCreated() throws Excepti

@Test
public void requestWhenIntrospectionConfiguredThenSessionIsNotCreated() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class, BasicController.class).autowire();
mockRestOperations(json("Active"));
this.spring.register(OpaqueTokenConfig.class, BasicController.class).autowire();
mockWebServer(json("Active"));
// @formatter:off
MvcResult result = this.mvc.perform(get("/authenticated").with(bearerToken("token")))
.andExpect(status().isOk())
Expand Down Expand Up @@ -1060,8 +1062,8 @@ public void getWhenDefaultAndCustomJwtAuthenticationManagerThenCustomUsed() thro

@Test
public void getWhenIntrospectingThenOk() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class, BasicController.class).autowire();
mockRestOperations(json("Active"));
this.spring.register(OpaqueTokenConfig.class, BasicController.class).autowire();
mockWebServer(json("Active"));
// @formatter:off
this.mvc.perform(get("/authenticated").with(bearerToken("token")))
.andExpect(status().isOk())
Expand All @@ -1071,9 +1073,8 @@ public void getWhenIntrospectingThenOk() throws Exception {

@Test
public void getWhenOpaqueTokenInLambdaAndIntrospectingThenOk() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenInLambdaConfig.class, BasicController.class)
.autowire();
mockRestOperations(json("Active"));
this.spring.register(OpaqueTokenInLambdaConfig.class, BasicController.class).autowire();
mockWebServer(json("Active"));
// @formatter:off
this.mvc.perform(get("/authenticated").with(bearerToken("token")))
.andExpect(status().isOk())
Expand All @@ -1083,8 +1084,8 @@ public void getWhenOpaqueTokenInLambdaAndIntrospectingThenOk() throws Exception

@Test
public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire();
mockRestOperations(json("Inactive"));
this.spring.register(OpaqueTokenConfig.class).autowire();
mockWebServer(json("Inactive"));
// @formatter:off
this.mvc.perform(get("/").with(bearerToken("token")))
.andExpect(status().isUnauthorized())
Expand All @@ -1094,8 +1095,8 @@ public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {

@Test
public void getWhenIntrospectionLacksScopeThenForbidden() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire();
mockRestOperations(json("ActiveNoScopes"));
this.spring.register(OpaqueTokenConfig.class).autowire();
mockWebServer(json("ActiveNoScopes"));
// @formatter:off
this.mvc.perform(get("/requires-read-scope").with(bearerToken("token")))
.andExpect(status().isForbidden())
Expand Down Expand Up @@ -1400,13 +1401,11 @@ public void getJwtAuthenticationConverterWhenDuplicateConverterBeansThenThrowsEx

@Test
public void getWhenCustomAuthenticationConverterThenUsed() throws Exception {
this.spring
.register(RestOperationsConfig.class, OpaqueTokenAuthenticationConverterConfig.class, BasicController.class)
.autowire();
this.spring.register(OpaqueTokenAuthenticationConverterConfig.class, BasicController.class).autowire();
OpaqueTokenAuthenticationConverter authenticationConverter = bean(OpaqueTokenAuthenticationConverter.class);
given(authenticationConverter.convert(anyString(), any(OAuth2AuthenticatedPrincipal.class)))
.willReturn(new TestingAuthenticationToken("jdoe", null, Collections.emptyList()));
mockRestOperations(json("Active"));
mockWebServer(json("Active"));
// @formatter:off
this.mvc.perform(get("/authenticated").with(bearerToken("token")))
.andExpect(status().isOk())
Expand Down Expand Up @@ -2343,6 +2342,7 @@ AuthenticationEventPublisher authenticationEventPublisher() {
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(OpaqueTokenIntrospectorConfig.class)
static class OpaqueTokenConfig {

@Bean
Expand All @@ -2364,6 +2364,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(OpaqueTokenIntrospectorConfig.class)
static class OpaqueTokenInLambdaConfig {

@Bean
Expand All @@ -2387,6 +2388,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

@Configuration
@EnableWebSecurity
@Import(OpaqueTokenIntrospectorConfig.class)
static class OpaqueTokenAuthenticationManagerConfig {

@Bean
Expand Down Expand Up @@ -2559,6 +2561,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(OpaqueTokenIntrospectorConfig.class)
static class OpaqueTokenAuthenticationConverterConfig {

@Bean
Expand All @@ -2583,6 +2586,20 @@ OpaqueTokenAuthenticationConverter authenticationConverter() {

}

@Configuration
@Import(WebServerConfig.class)
static class OpaqueTokenIntrospectorConfig {

@Value("${mockwebserver.url:https://example.org}")
String introspectUri;

@Bean
SpringOpaqueTokenIntrospector introspector() {
return new SpringOpaqueTokenIntrospector(this.introspectUri, new RestTemplate());
}

}

@Configuration
static class JwtDecoderConfig {

Expand Down Expand Up @@ -2694,11 +2711,6 @@ NimbusJwtDecoder jwtDecoder() {
.build();
}

@Bean
NimbusOpaqueTokenIntrospector tokenIntrospectionClient() {
return new NimbusOpaqueTokenIntrospector("https://example.org/introspect", this.rest);
}

}

private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -86,9 +86,9 @@
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;
Expand All @@ -99,6 +99,7 @@
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
Expand Down Expand Up @@ -659,8 +660,8 @@ public void requestWhenUsingPublicKeyAlgorithmDoesNotMatchThenReturnsInvalidToke

@Test
public void getWhenIntrospectingThenOk() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("Active"));
this.spring.configLocations(xml("OpaqueTokenWebServer"), xml("OpaqueToken")).autowire();
mockWebServer(json("Active"));
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token"))
.andExpect(status().isNotFound());
Expand All @@ -669,9 +670,9 @@ public void getWhenIntrospectingThenOk() throws Exception {

@Test
public void configureWhenIntrospectingWithAuthenticationConverterThenUses() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueTokenAndAuthenticationConverter"))
this.spring.configLocations(xml("OpaqueTokenWebServer"), xml("OpaqueTokenAndAuthenticationConverter"))
.autowire();
mockRestOperations(json("Active"));
mockWebServer(json("Active"));
OpaqueTokenAuthenticationConverter converter = bean(OpaqueTokenAuthenticationConverter.class);
given(converter.convert(any(), any())).willReturn(new TestingAuthenticationToken("user", "pass", "app"));
// @formatter:off
Expand All @@ -683,8 +684,8 @@ public void configureWhenIntrospectingWithAuthenticationConverterThenUses() thro

@Test
public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("Inactive"));
this.spring.configLocations(xml("OpaqueTokenWebServer"), xml("OpaqueToken")).autowire();
mockWebServer(json("Inactive"));
// @formatter:off
MockHttpServletRequestBuilder request = get("/")
.header("Authorization", "Bearer token");
Expand All @@ -696,8 +697,8 @@ public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {

@Test
public void getWhenIntrospectionLacksScopeThenForbidden() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("ActiveNoScopes"));
this.spring.configLocations(xml("OpaqueTokenWebServer"), xml("OpaqueToken")).autowire();
mockWebServer(json("ActiveNoScopes"));
// @formatter:off
this.mvc.perform(get("/requires-read-scope").header("Authorization", "Bearer token"))
.andExpect(status().isForbidden())
Expand Down Expand Up @@ -1028,20 +1029,20 @@ public void setRest(RestOperations rest) {

static class OpaqueTokenIntrospectorFactoryBean implements FactoryBean<OpaqueTokenIntrospector> {

private RestOperations rest;
private String introspectionUri;

@Override
public OpaqueTokenIntrospector getObject() throws Exception {
return new NimbusOpaqueTokenIntrospector("https://idp.example.org", this.rest);
return new SpringOpaqueTokenIntrospector(this.introspectionUri, new RestTemplate());
}

@Override
public Class<?> getObjectType() {
return OpaqueTokenIntrospector.class;
}

public void setRest(RestOperations rest) {
this.rest = rest;
public void setIntrospectionUri(String introspectionUri) {
this.introspectionUri = introspectionUri;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,7 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
Expand All @@ -41,7 +42,6 @@ import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrinci
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens
import org.springframework.security.oauth2.jwt.JwtClaimNames
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector
import org.springframework.security.web.SecurityFilterChain
Expand Down Expand Up @@ -84,14 +84,16 @@ class OpaqueTokenDslTests {
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_JSON
}
val entity = ResponseEntity("{\n" +
" \"active\" : true,\n" +
" \"sub\": \"test-subject\",\n" +
" \"scope\": \"message:read\",\n" +
" \"exp\": 4683883211\n" +
"}", headers, HttpStatus.OK)
val entity = ResponseEntity(
mapOf<String, Any>(
"active" to true,
"sub" to "test-subject",
"scope" to "message:read",
"exp" to 4683883211
), headers, HttpStatus.OK
)
every {
DefaultOpaqueConfig.REST.exchange(any(), eq(String::class.java))
DefaultOpaqueConfig.REST.exchange(any(), any(ParameterizedTypeReference::class))
} returns entity

this.mockMvc.get("/authenticated") {
Expand Down Expand Up @@ -127,8 +129,8 @@ class OpaqueTokenDslTests {
open fun rest(): RestOperations = REST

@Bean
open fun tokenIntrospectionClient(): NimbusOpaqueTokenIntrospector {
return NimbusOpaqueTokenIntrospector("https://example.org/introspect", REST)
open fun tokenIntrospectionClient(): SpringOpaqueTokenIntrospector {
return SpringOpaqueTokenIntrospector("https://example.org/introspect", REST)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2020 the original author or authors.
~ Copyright 2002-2025 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
Expand All @@ -21,12 +21,10 @@
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

<b:bean name="rest" class="org.mockito.Mockito" factory-method="mock">
<b:constructor-arg value="org.springframework.web.client.RestOperations" type="java.lang.Class"/>
</b:bean>

<b:bean name="introspector"
class="org.springframework.security.config.http.OAuth2ResourceServerBeanDefinitionParserTests$OpaqueTokenIntrospectorFactoryBean">
<b:property name="rest" ref="rest"/>
<b:property name="introspectionUri" value="${introspection-uri}"/>
</b:bean>

<b:import resource="OAuth2ResourceServerBeanDefinitionParserTests-WebServer.xml"/>
</b:beans>
Loading