diff --git a/docs/modules/ROOT/pages/features/integrations/rest/http-interface.adoc b/docs/modules/ROOT/pages/features/integrations/rest/http-interface.adoc index db69fd591d..1d818f520b 100644 --- a/docs/modules/ROOT/pages/features/integrations/rest/http-interface.adoc +++ b/docs/modules/ROOT/pages/features/integrations/rest/http-interface.adoc @@ -5,7 +5,7 @@ Spring Security's OAuth Support can integrate with `RestClient` and `WebClient` [[configuration]] == Configuration -After xref:features/integrations/rest/http-interface.adoc#configuration-restclient[RestClient] or xref:features/integrations/rest/http-interface.adoc#configuration-webclient[WebClient] specific configuration, usage of xref:features/integrations/rest/http-interface.adoc[] only requires adding a xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] to methods that require OAuth. +After xref:features/integrations/rest/http-interface.adoc#configuration-restclient[RestClient] or xref:features/integrations/rest/http-interface.adoc#configuration-webclient[WebClient] specific configuration, usage of xref:features/integrations/rest/http-interface.adoc[] only requires adding a xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] to methods that require OAuth or their declaring HTTP interface. Since the presence of xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] determines if and how the OAuth token will be resolved, it is safe to add Spring Security's OAuth support any configuration. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index be7c6daddf..a613e8fbec 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -49,6 +49,7 @@ http.csrf((csrf) -> csrf.spa()); * Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration] * Added support for custom `JwkSource` in `NimbusJwtDecoder`, allowing usage of Nimbus's `JwkSourceBuilder` API * Added builder for `NimbusJwtEncoder`, supports specifying an EC or RSA key pair or a secret key +* Added support for `@ClientRegistrationId` at class level, eliminating the need for method level repetition == SAML 2.0 diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessor.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessor.java index e33d897225..ba6e6786b9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessor.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessor.java @@ -17,12 +17,12 @@ package org.springframework.security.oauth2.client.web.client; import java.lang.reflect.Method; -import java.util.Optional; import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.core.annotation.SecurityAnnotationScanner; +import org.springframework.security.core.annotation.SecurityAnnotationScanners; import org.springframework.security.oauth2.client.annotation.ClientRegistrationId; import org.springframework.security.oauth2.client.web.ClientAttributes; import org.springframework.web.service.invoker.HttpRequestValues; @@ -38,15 +38,13 @@ public final class ClientRegistrationIdProcessor implements HttpRequestValues.Pr public static ClientRegistrationIdProcessor DEFAULT_INSTANCE = new ClientRegistrationIdProcessor(); - private ClientRegistrationIdProcessor() { - } + private SecurityAnnotationScanner securityAnnotationScanner = SecurityAnnotationScanners + .requireUnique(ClientRegistrationId.class); @Override public void process(Method method, MethodParameter[] parameters, @Nullable Object[] arguments, HttpRequestValues.Builder builder) { - ClientRegistrationId registeredId = Optional - .ofNullable(AnnotationUtils.findAnnotation(method, ClientRegistrationId.class)) - .orElseGet(() -> AnnotationUtils.findAnnotation(method.getDeclaringClass(), ClientRegistrationId.class)); + ClientRegistrationId registeredId = this.securityAnnotationScanner.scan(method, method.getDeclaringClass()); if (registeredId != null) { String registrationId = registeredId.registrationId(); @@ -54,4 +52,7 @@ public final class ClientRegistrationIdProcessor implements HttpRequestValues.Pr } } + private ClientRegistrationIdProcessor() { + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorTests.java index b23f133607..7f5f12e17e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorTests.java @@ -22,12 +22,14 @@ import java.lang.reflect.Method; import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.oauth2.client.annotation.ClientRegistrationId; import org.springframework.security.oauth2.client.web.ClientAttributes; import org.springframework.util.ReflectionUtils; import org.springframework.web.service.invoker.HttpRequestValues; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Unit tests for {@link ClientRegistrationIdProcessor}. @@ -56,9 +58,8 @@ class ClientRegistrationIdProcessorTests { @Test void processWhenMetaClientRegistrationIdPresentThenSet() { HttpRequestValues.Builder builder = HttpRequestValues.builder(); - Method hasMetaClientRegistrationId = ReflectionUtils.findMethod(RestService.class, - "hasMetaClientRegistrationId"); - this.processor.process(hasMetaClientRegistrationId, null, null, builder); + Method hasClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "hasMetaClientRegistrationId"); + this.processor.process(hasClientRegistrationId, null, null, builder); String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes()); assertThat(registrationId).isEqualTo(REGISTRATION_ID); @@ -67,8 +68,8 @@ class ClientRegistrationIdProcessorTests { @Test void processWhenNoClientRegistrationIdPresentThenNull() { HttpRequestValues.Builder builder = HttpRequestValues.builder(); - Method noClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "noClientRegistrationId"); - this.processor.process(noClientRegistrationId, null, null, builder); + Method hasClientRegistrationId = ReflectionUtils.findMethod(RestService.class, "noClientRegistrationId"); + this.processor.process(hasClientRegistrationId, null, null, builder); String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes()); assertThat(registrationId).isNull(); @@ -77,7 +78,7 @@ class ClientRegistrationIdProcessorTests { @Test void processWhenClientRegistrationIdPresentOnDeclaringClassThenSet() { HttpRequestValues.Builder builder = HttpRequestValues.builder(); - Method declaringClassHasClientRegistrationId = ReflectionUtils.findMethod(AnnotatedRestService.class, + Method declaringClassHasClientRegistrationId = ReflectionUtils.findMethod(TypeAnnotatedRestService.class, "declaringClassHasClientRegistrationId"); this.processor.process(declaringClassHasClientRegistrationId, null, null, builder); @@ -85,6 +86,16 @@ class ClientRegistrationIdProcessorTests { assertThat(registrationId).isEqualTo(REGISTRATION_ID); } + @Test + void processWhenDuplicateClientRegistrationIdPresentOnAggregateServiceThenException() { + HttpRequestValues.Builder builder = HttpRequestValues.builder(); + Method shouldFailDueToDuplicateClientRegistrationId = ReflectionUtils.findMethod(AggregateRestService.class, + "shouldFailDueToDuplicateClientRegistrationId"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> this.processor.process(shouldFailDueToDuplicateClientRegistrationId, null, null, builder)); + } + interface RestService { @ClientRegistrationId(REGISTRATION_ID) @@ -104,10 +115,26 @@ class ClientRegistrationIdProcessorTests { } @ClientRegistrationId(REGISTRATION_ID) - interface AnnotatedRestService { + interface TypeAnnotatedRestService { void declaringClassHasClientRegistrationId(); } + @ClientRegistrationId("a") + interface ARestService { + + } + + @ClientRegistrationId("b") + interface BRestService { + + } + + interface AggregateRestService extends ARestService, BRestService { + + void shouldFailDueToDuplicateClientRegistrationId(); + + } + }