Skip to content

Remove Nimbus(Reactive)OpaqueTokenIntrospector #17326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.JSONObjectUtils;
import jakarta.servlet.http.HttpServletRequest;
import net.minidev.json.JSONObject;
import okhttp3.mockwebserver.MockResponse;
Expand All @@ -57,6 +58,7 @@
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.beans.factory.xml.XmlReaderContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
Expand Down Expand Up @@ -84,9 +86,9 @@
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
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.security.web.authentication.AuthenticationConverter;
Expand Down Expand Up @@ -139,7 +141,7 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
@Test
public void getWhenValidBearerTokenThenAcceptsRequest() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -150,7 +152,7 @@ public void getWhenValidBearerTokenThenAcceptsRequest() throws Exception {
@Test
public void getWhenCustomSecurityContextHolderStrategyThenUses() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("JwtCustomSecurityContextHolderStrategy")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -175,7 +177,7 @@ public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
@Test
public void getWhenExpiredBearerTokenThenInvalidToken() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("Expired");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -187,7 +189,7 @@ public void getWhenExpiredBearerTokenThenInvalidToken() throws Exception {
@Test
public void getWhenBadJwkEndpointThen500() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations("malformed");
mockJwksRestOperations("malformed");
String token = this.token("ValidNoScopes");
// @formatter:off
assertThatExceptionOfType(AuthenticationServiceException.class)
Expand Down Expand Up @@ -219,7 +221,7 @@ public void getWhenMalformedBearerTokenThenInvalidToken() throws Exception {
@Test
public void getWhenMalformedPayloadThenInvalidToken() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("MalformedPayload");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -242,7 +244,7 @@ public void getWhenUnsignedBearerTokenThenInvalidToken() throws Exception {
@Test
public void getWhenBearerTokenBeforeNotBeforeThenInvalidToken() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
this.mockRestOperations(jwks("Default"));
this.mockJwksRestOperations(jwks("Default"));
String token = this.token("TooEarly");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand Down Expand Up @@ -299,7 +301,7 @@ public void getWhenNoBearerTokenThenUnauthorized() throws Exception {
@Test
public void getWhenSufficientlyScopedBearerTokenThenAcceptsRequest() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidMessageReadScope");
// @formatter:off
this.mvc.perform(get("/requires-read-scope").header("Authorization", "Bearer " + token))
Expand All @@ -310,7 +312,7 @@ public void getWhenSufficientlyScopedBearerTokenThenAcceptsRequest() throws Exce
@Test
public void getWhenInsufficientScopeThenInsufficientScopeError() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/requires-read-scope").header("Authorization", "Bearer " + token))
Expand All @@ -322,7 +324,7 @@ public void getWhenInsufficientScopeThenInsufficientScopeError() throws Exceptio
@Test
public void getWhenInsufficientScpThenInsufficientScopeError() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidMessageWriteScp");
// @formatter:off
this.mvc.perform(get("/requires-read-scope").header("Authorization", "Bearer " + token))
Expand All @@ -334,7 +336,7 @@ public void getWhenInsufficientScpThenInsufficientScopeError() throws Exception
@Test
public void getWhenAuthorizationServerHasNoMatchingKeyThenInvalidToken() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Empty"));
mockJwksRestOperations(jwks("Empty"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -346,7 +348,7 @@ public void getWhenAuthorizationServerHasNoMatchingKeyThenInvalidToken() throws
@Test
public void getWhenAuthorizationServerHasMultipleMatchingKeysThenOk() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("TwoKeys"));
mockJwksRestOperations(jwks("TwoKeys"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + token))
Expand All @@ -357,7 +359,7 @@ public void getWhenAuthorizationServerHasMultipleMatchingKeysThenOk() throws Exc
@Test
public void getWhenKeyMatchesByKidThenOk() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("TwoKeys"));
mockJwksRestOperations(jwks("TwoKeys"));
String token = this.token("Kid");
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + token))
Expand All @@ -368,7 +370,7 @@ public void getWhenKeyMatchesByKidThenOk() throws Exception {
@Test
public void postWhenValidBearerTokenAndNoCsrfTokenThenOk() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(post("/authenticated").header("Authorization", "Bearer " + token))
Expand All @@ -390,7 +392,7 @@ public void postWhenNoBearerTokenThenCsrfDenies() throws Exception {
@Test
public void postWhenExpiredBearerTokenAndNoCsrfThenInvalidToken() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("Expired");
// @formatter:off
this.mvc.perform(post("/authenticated").header("Authorization", "Bearer " + token))
Expand All @@ -402,7 +404,7 @@ public void postWhenExpiredBearerTokenAndNoCsrfThenInvalidToken() throws Excepti
@Test
public void requestWhenJwtThenSessionIsNotCreated() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
MvcResult result = this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand Down Expand Up @@ -438,7 +440,7 @@ public void requestWhenNoBearerTokenThenSessionIsCreated() throws Exception {
@Test
public void requestWhenSessionManagementConfiguredThenUses() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("AlwaysSessionCreation")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
MvcResult result = this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand Down Expand Up @@ -587,7 +589,7 @@ public void requestWhenRealmNameConfiguredThenUsesOnAccessDenied() throws Except
@Test
public void requestWhenCustomJwtValidatorFailsThenCorrespondingErrorMessage() throws Exception {
this.spring.configLocations(xml("MockJwtValidator"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
OAuth2TokenValidator<Jwt> jwtValidator = this.spring.getContext().getBean(OAuth2TokenValidator.class);
OAuth2Error error = new OAuth2Error("custom-error", "custom-description", "custom-uri");
Expand All @@ -602,7 +604,7 @@ public void requestWhenCustomJwtValidatorFailsThenCorrespondingErrorMessage() th
@Test
public void requestWhenClockSkewSetThenTimestampWindowRelaxedAccordingly() throws Exception {
this.spring.configLocations(xml("UnexpiredJwtClockSkew"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ExpiresAt4687177990");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand All @@ -613,7 +615,7 @@ public void requestWhenClockSkewSetThenTimestampWindowRelaxedAccordingly() throw
@Test
public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired() throws Exception {
this.spring.configLocations(xml("ExpiredJwtClockSkew"), xml("Jwt")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ExpiresAt4687177990");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
Expand Down Expand Up @@ -675,7 +677,7 @@ public void requestWhenUsingPublicKeyAlgorithmDoesNotMatchThenReturnsInvalidToke
@Test
public void getWhenIntrospectingThenOk() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("Active"));
mockJsonRestOperations(json("Active"));
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer token"))
.andExpect(status().isNotFound());
Expand All @@ -686,7 +688,7 @@ public void getWhenIntrospectingThenOk() throws Exception {
public void configureWhenIntrospectingWithAuthenticationConverterThenUses() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueTokenAndAuthenticationConverter"))
.autowire();
mockRestOperations(json("Active"));
mockJsonRestOperations(json("Active"));
OpaqueTokenAuthenticationConverter converter = bean(OpaqueTokenAuthenticationConverter.class);
given(converter.convert(any(), any())).willReturn(new TestingAuthenticationToken("user", "pass", "app"));
// @formatter:off
Expand All @@ -699,7 +701,7 @@ public void configureWhenIntrospectingWithAuthenticationConverterThenUses() thro
@Test
public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("Inactive"));
mockJsonRestOperations(json("Inactive"));
// @formatter:off
MockHttpServletRequestBuilder request = get("/")
.header("Authorization", "Bearer token");
Expand All @@ -712,7 +714,7 @@ public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
@Test
public void getWhenIntrospectionLacksScopeThenForbidden() throws Exception {
this.spring.configLocations(xml("OpaqueTokenRestOperations"), xml("OpaqueToken")).autowire();
mockRestOperations(json("ActiveNoScopes"));
mockJsonRestOperations(json("ActiveNoScopes"));
// @formatter:off
this.mvc.perform(get("/requires-read-scope").header("Authorization", "Bearer token"))
.andExpect(status().isForbidden())
Expand Down Expand Up @@ -818,7 +820,7 @@ public void requestWhenFormLoginAndResourceServerEntryPointsThenSessionCreatedBy
@Test
public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages() throws Exception {
this.spring.configLocations(xml("JwtRestOperations"), xml("BasicAndResourceServer")).autowire();
mockRestOperations(jwks("Default"));
mockJwksRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + token))
Expand Down Expand Up @@ -963,14 +965,29 @@ private void mockWebServer(String response) {
.setBody(response));
}

private void mockRestOperations(String response) {
private void mockJwksRestOperations(String response) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you able to place this change in a separate commit? In this way, the deprecation commit only contains the changes needed for deprecation to happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your feedback. I've updated it

RestOperations rest = this.spring.getContext().getBean(RestOperations.class);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> entity = new ResponseEntity<>(response, headers, HttpStatus.OK);
given(rest.exchange(any(RequestEntity.class), eq(String.class))).willReturn(entity);
}

private void mockJsonRestOperations(String response) {
try {
RestOperations rest = this.spring.getContext().getBean(RestOperations.class);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<Map<String, Object>> entity = new ResponseEntity<>(JSONObjectUtils.parse(response), headers,
HttpStatus.OK);
given(rest.exchange(any(RequestEntity.class), eq(new ParameterizedTypeReference<Map<String, Object>>() {
}))).willReturn(entity);
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}

private String json(String name) throws IOException {
return resource(name + ".json");
}
Expand Down Expand Up @@ -1047,7 +1064,7 @@ static class OpaqueTokenIntrospectorFactoryBean implements FactoryBean<OpaqueTok

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

@Override
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,15 +84,15 @@ 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 responseBody: Map<String, Any> = mapOf(
"active" to true,
"sub" to "test-subject",
"scope" to "message:read",
"exp" to 4683883211
)
every {
DefaultOpaqueConfig.REST.exchange(any(), eq(String::class.java))
} returns entity
DefaultOpaqueConfig.REST.exchange(any(), any<ParameterizedTypeReference<Map<String, Any>>>())
} returns ResponseEntity(responseBody, headers, HttpStatus.OK)

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

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

Expand Down
Loading
Loading