From 97949521535695bcd04262d1eb546e16cc3330ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:00:34 +0000 Subject: [PATCH 01/31] Bump org-aspectj from 1.9.21.1 to 1.9.21.2 Bumps `org-aspectj` from 1.9.21.1 to 1.9.21.2. Updates `org.aspectj:aspectjrt` from 1.9.21.1 to 1.9.21.2 - [Release notes](https://github.com/eclipse/org.aspectj/releases) - [Commits](https://github.com/eclipse/org.aspectj/commits) Updates `org.aspectj:aspectjweaver` from 1.9.21.1 to 1.9.21.2 - [Release notes](https://github.com/eclipse/org.aspectj/releases) - [Commits](https://github.com/eclipse/org.aspectj/commits) --- updated-dependencies: - dependency-name: org.aspectj:aspectjrt dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.aspectj:aspectjweaver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dea55526280..35ba18d76ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ io-spring-nohttp = "0.0.11" jakarta-websocket = "2.1.1" org-apache-directory-server = "1.5.5" org-apache-maven-resolver = "1.8.2" -org-aspectj = "1.9.21.1" +org-aspectj = "1.9.21.2" org-bouncycastle = "1.70" org-eclipse-jetty = "11.0.20" org-jetbrains-kotlin = "1.8.22" From f45be51987b49e219f217a17f4b1e531c427656c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:00:44 +0000 Subject: [PATCH 02/31] Bump org-aspectj from 1.9.21.1 to 1.9.21.2 Bumps `org-aspectj` from 1.9.21.1 to 1.9.21.2. Updates `org.aspectj:aspectjrt` from 1.9.21.1 to 1.9.21.2 - [Release notes](https://github.com/eclipse/org.aspectj/releases) - [Commits](https://github.com/eclipse/org.aspectj/commits) Updates `org.aspectj:aspectjweaver` from 1.9.21.1 to 1.9.21.2 - [Release notes](https://github.com/eclipse/org.aspectj/releases) - [Commits](https://github.com/eclipse/org.aspectj/commits) --- updated-dependencies: - dependency-name: org.aspectj:aspectjrt dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.aspectj:aspectjweaver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 057c3c6cdcf..2fe42b6e1d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ io-spring-nohttp = "0.0.11" jakarta-websocket = "2.1.1" org-apache-directory-server = "1.5.5" org-apache-maven-resolver = "1.9.18" -org-aspectj = "1.9.21.1" +org-aspectj = "1.9.21.2" org-bouncycastle = "1.70" org-eclipse-jetty = "11.0.20" org-jetbrains-kotlin = "1.9.23" From d2f1e39095a809a1b6b923346e4f4f1a298fe434 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:09:07 +0000 Subject: [PATCH 03/31] Bump org.mockito:mockito-bom from 5.5.0 to 5.11.0 Bumps [org.mockito:mockito-bom](https://github.com/mockito/mockito) from 5.5.0 to 5.11.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.5.0...v5.11.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c34064a7a0b..f5ba04cf5c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ org-bouncycastle = "1.70" org-eclipse-jetty = "11.0.20" org-jetbrains-kotlin = "1.9.23" org-jetbrains-kotlinx = "1.8.0" -org-mockito = "5.5.0" +org-mockito = "5.11.0" org-opensaml = "4.3.0" org-springframework = "6.1.4" From c0928bf1980ade54cd8a2d40499eeab05d50b4e7 Mon Sep 17 00:00:00 2001 From: ruabtmh Date: Wed, 6 Mar 2024 00:32:15 +0300 Subject: [PATCH 04/31] Add DelegatingAuthenticationConverter Closes gh-14644 --- .../DelegatingAuthenticationConverter.java | 60 ++++++++ ...elegatingAuthenticationConverterTests.java | 135 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverter.java create mode 100644 web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverterTests.java diff --git a/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverter.java new file mode 100644 index 00000000000..f10e3cffcb3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * A {@link AuthenticationConverter}, that iterates over multiple + * {@link AuthenticationConverter}. The first non-null {@link Authentication} will be used + * as a result. + * + * @author Max Batischev + * @since 6.3 + */ +public final class DelegatingAuthenticationConverter implements AuthenticationConverter { + + private final List delegates; + + public DelegatingAuthenticationConverter(List delegates) { + Assert.notEmpty(delegates, "delegates cannot be null"); + this.delegates = new ArrayList<>(delegates); + } + + public DelegatingAuthenticationConverter(AuthenticationConverter... delegates) { + Assert.notEmpty(delegates, "delegates cannot be null"); + this.delegates = List.of(delegates); + } + + @Override + public Authentication convert(HttpServletRequest request) { + for (AuthenticationConverter delegate : this.delegates) { + Authentication authentication = delegate.convert(request); + if (authentication != null) { + return authentication; + } + } + return null; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverterTests.java new file mode 100644 index 00000000000..6de7ef325ae --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationConverterTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.test.web.CodecTestUtils; +import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link DelegatingAuthenticationConverter}. + * + * @author Max Batischev + */ +@ExtendWith(MockitoExtension.class) +public class DelegatingAuthenticationConverterTests { + + private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token"; + + private static final String TEST_X_AUTH_TOKEN = "test-x-auth-token"; + + private static final String TEST_CUSTOM_PRINCIPAL = "test_custom_principal"; + + private static final String TEST_CUSTOM_CREDENTIALS = "test_custom_credentials"; + + private static final String TEST_BASIC_CREDENTIALS = "username:password"; + + private static final String INVALID_BASIC_CREDENTIALS = "invalid_credentials"; + + private DelegatingAuthenticationConverter converter; + + @Mock + private AuthenticationDetailsSource authenticationDetailsSource; + + @Test + public void requestWhenBasicAuthorizationHeaderIsPresentThenAuthenticates() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64(TEST_BASIC_CREDENTIALS)); + this.converter = new DelegatingAuthenticationConverter( + new BasicAuthenticationConverter(this.authenticationDetailsSource), + new TestNullableAuthenticationConverter()); + + Authentication authentication = this.converter.convert(request); + + assertThat(authentication).isNotNull(); + assertThat(authentication.getName()).isEqualTo("username"); + } + + @Test + public void requestWhenXAuthHeaderIsPresentThenAuthenticates() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(X_AUTH_TOKEN_HEADER, TEST_X_AUTH_TOKEN); + this.converter = new DelegatingAuthenticationConverter(new TestAuthenticationConverter(), + new TestNullableAuthenticationConverter()); + + Authentication authentication = this.converter.convert(request); + + assertThat(authentication).isNotNull(); + assertThat(authentication.getName()).isEqualTo(TEST_CUSTOM_PRINCIPAL); + } + + @Test + public void requestWhenXAuthHeaderIsPresentThenDoesntAuthenticate() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(X_AUTH_TOKEN_HEADER, TEST_X_AUTH_TOKEN); + this.converter = new DelegatingAuthenticationConverter(new TestNullableAuthenticationConverter()); + + Authentication authentication = this.converter.convert(request); + + assertThat(authentication).isNull(); + } + + @Test + public void requestWhenInvalidBasicAuthorizationTokenThenError() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64(INVALID_BASIC_CREDENTIALS)); + this.converter = new DelegatingAuthenticationConverter( + new BasicAuthenticationConverter(this.authenticationDetailsSource), + new TestNullableAuthenticationConverter()); + + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> this.converter.convert(request)); + } + + private static class TestAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + String header = request.getHeader(X_AUTH_TOKEN_HEADER); + if (header != null) { + return new TestingAuthenticationToken(TEST_CUSTOM_PRINCIPAL, TEST_CUSTOM_CREDENTIALS); + } + else { + return null; + } + } + + } + + private static class TestNullableAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + return null; + } + + } + +} From 52dfbfb5b3c4325df96de751c4ab6ad4d866d240 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 29 Feb 2024 14:14:02 -0700 Subject: [PATCH 05/31] Add Authorization Proxy Support Closes gh-14596 --- .../AuthorizationProxyConfiguration.java | 44 ++ .../Jsr250MethodSecurityConfiguration.java | 3 +- .../configuration/MethodSecuritySelector.java | 1 + .../PrePostMethodSecurityConfiguration.java | 9 +- .../SecuredMethodSecurityConfiguration.java | 3 +- .../AuthorizationProxyConfigurationTests.java | 92 ++++ .../AuthorizationAdvisorProxyFactory.java | 308 +++++++++++++ .../AuthorizationProxyFactory.java | 40 ++ .../method/AuthorizationAdvisor.java | 37 ++ ...rizationManagerAfterMethodInterceptor.java | 6 +- ...izationManagerBeforeMethodInterceptor.java | 6 +- ...tFilterAuthorizationMethodInterceptor.java | 6 +- ...eFilterAuthorizationMethodInterceptor.java | 6 +- ...AuthorizationAdvisorProxyFactoryTests.java | 431 ++++++++++++++++++ .../authorization/method-security.adoc | 391 ++++++++++++++++ docs/modules/ROOT/pages/whats-new.adoc | 4 + 16 files changed, 1360 insertions(+), 27 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java create mode 100644 core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java new file mode 100644 index 00000000000..6aa247c6670 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisor; + +@Configuration(proxyBeanMethods = false) +final class AuthorizationProxyConfiguration implements AopInfrastructureBean { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider provider) { + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + AnnotationAwareOrderComparator.sort(advisors); + return new AuthorizationAdvisorProxyFactory(advisors); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java index 39567ddfdca..45908fb549a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java @@ -20,6 +20,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; @@ -48,7 +49,7 @@ */ @Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) -final class Jsr250MethodSecurityConfiguration implements ImportAware { +final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean { private int interceptorOrderOffset; diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index 4b561360a73..928ed485484 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -56,6 +56,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { if (annotation.jsr250Enabled()) { imports.add(Jsr250MethodSecurityConfiguration.class.getName()); } + imports.add(AuthorizationProxyConfiguration.class.getName()); return imports.toArray(new String[0]); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 4c10dc4e5b6..7fea76850df 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -27,7 +27,6 @@ import org.jetbrains.annotations.Nullable; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; @@ -36,7 +35,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.Role; -import org.springframework.core.Ordered; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; @@ -44,6 +42,7 @@ import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; @@ -65,7 +64,7 @@ */ @Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) -final class PrePostMethodSecurityConfiguration implements ImportAware { +final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean { private int interceptorOrderOffset; @@ -175,8 +174,8 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { this.interceptorOrderOffset = annotation.offset(); } - private static final class DeferringMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { + private static final class DeferringMethodInterceptor + implements AuthorizationAdvisor { private final Pointcut pointcut; diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java index a1909388781..2b6a2e29280 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java @@ -20,6 +20,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; @@ -48,7 +49,7 @@ */ @Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) -final class SecuredMethodSecurityConfiguration implements ImportAware { +final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean { private int interceptorOrderOffset; diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java new file mode 100644 index 00000000000..e3e41c03167 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link PrePostMethodSecurityConfiguration}. + * + * @author Evgeniy Cheban + * @author Josh Cummings + */ +@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class }) +@SecurityTestExecutionListeners +public class AuthorizationProxyConfigurationTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + AuthorizationProxyFactory proxyFactory; + + @WithMockUser + @Test + public void proxyWhenNotPreAuthorizedThenDenies() { + this.spring.register(DefaultsConfig.class).autowire(); + Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::makeToast) + .withMessage("Access Denied"); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(toaster::extractBread) + .withMessage("Access Denied"); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void proxyWhenPreAuthorizedThenAllows() { + this.spring.register(DefaultsConfig.class).autowire(); + Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster()); + toaster.makeToast(); + assertThat(toaster.extractBread()).isEqualTo("yummy"); + } + + @EnableMethodSecurity + @Configuration + static class DefaultsConfig { + + } + + static class Toaster { + + @PreAuthorize("hasRole('ADMIN')") + void makeToast() { + + } + + @PostAuthorize("hasRole('ADMIN')") + String extractBread() { + return "yummy"; + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java new file mode 100644 index 00000000000..3fbb8df980f --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java @@ -0,0 +1,308 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.lang.reflect.Array; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.util.ClassUtils; + +/** + * A proxy factory for applying authorization advice to an arbitrary object. + * + *

+ * For example, consider a non-Spring-managed object {@code Foo}:

+ *     class Foo {
+ *         @PreAuthorize("hasAuthority('bar:read')")
+ *         String bar() { ... }
+ *     }
+ * 
+ * + * Use {@link AuthorizationAdvisorProxyFactory} to wrap the instance in Spring Security's + * {@link org.springframework.security.access.prepost.PreAuthorize} method interceptor + * like so: + * + *
+ *     AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ *     AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
+ *     Foo foo = new Foo();
+ *     foo.bar(); // passes
+ *     Foo securedFoo = proxyFactory.proxy(foo);
+ *     securedFoo.bar(); // access denied!
+ * 
+ * + * @author Josh Cummings + * @since 6.3 + */ +public final class AuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory { + + private final Collection advisors; + + public AuthorizationAdvisorProxyFactory(AuthorizationAdvisor... advisors) { + this.advisors = List.of(advisors); + } + + public AuthorizationAdvisorProxyFactory(Collection advisors) { + this.advisors = List.copyOf(advisors); + } + + /** + * Create a new {@link AuthorizationAdvisorProxyFactory} that includes the given + * advisors in addition to any advisors {@code this} instance already has. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + * @return a new {@link AuthorizationAdvisorProxyFactory} instance + */ + public AuthorizationAdvisorProxyFactory withAdvisors(AuthorizationAdvisor... advisors) { + List merged = new ArrayList<>(this.advisors.size() + advisors.length); + merged.addAll(this.advisors); + merged.addAll(List.of(advisors)); + AnnotationAwareOrderComparator.sort(merged); + return new AuthorizationAdvisorProxyFactory(merged); + } + + /** + * Proxy an object to enforce authorization advice. + * + *

+ * Proxies any instance of a non-final class or a class that implements more than one + * interface. + * + *

+ * If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array}, + * {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is + * proxied. + * + *

+ * If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is + * invoked instead. + * @param target the instance to proxy + * @return the proxied instance + */ + @Override + public Object proxy(Object target) { + if (target == null) { + return null; + } + if (target instanceof Class targetClass) { + return proxyClass(targetClass); + } + if (target instanceof Iterator iterator) { + return proxyIterator(iterator); + } + if (target instanceof Queue queue) { + return proxyQueue(queue); + } + if (target instanceof List list) { + return proxyList(list); + } + if (target instanceof SortedSet set) { + return proxySortedSet(set); + } + if (target instanceof Set set) { + return proxySet(set); + } + if (target.getClass().isArray()) { + return proxyArray((Object[]) target); + } + if (target instanceof SortedMap map) { + return proxySortedMap(map); + } + if (target instanceof Iterable iterable) { + return proxyIterable(iterable); + } + if (target instanceof Map map) { + return proxyMap(map); + } + if (target instanceof Stream stream) { + return proxyStream(stream); + } + if (target instanceof Optional optional) { + return proxyOptional(optional); + } + ProxyFactory factory = new ProxyFactory(target); + for (Advisor advisor : this.advisors) { + factory.addAdvisors(advisor); + } + factory.setProxyTargetClass(!Modifier.isFinal(target.getClass().getModifiers())); + return factory.getProxy(); + } + + @SuppressWarnings("unchecked") + private T proxyCast(T target) { + return (T) proxy(target); + } + + private Class proxyClass(Class targetClass) { + ProxyFactory factory = new ProxyFactory(); + factory.setTargetClass(targetClass); + factory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass)); + factory.setProxyTargetClass(!Modifier.isFinal(targetClass.getModifiers())); + for (Advisor advisor : this.advisors) { + factory.addAdvisors(advisor); + } + return factory.getProxyClass(getClass().getClassLoader()); + } + + private Iterable proxyIterable(Iterable iterable) { + return () -> proxyIterator(iterable.iterator()); + } + + private Iterator proxyIterator(Iterator iterator) { + return new Iterator<>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return proxyCast(iterator.next()); + } + }; + } + + private SortedSet proxySortedSet(SortedSet set) { + SortedSet proxies = new TreeSet<>(set.comparator()); + for (T toProxy : set) { + proxies.add(proxyCast(toProxy)); + } + try { + set.clear(); + set.addAll(proxies); + return proxies; + } + catch (UnsupportedOperationException ex) { + return Collections.unmodifiableSortedSet(proxies); + } + } + + private Set proxySet(Set set) { + Set proxies = new LinkedHashSet<>(set.size()); + for (T toProxy : set) { + proxies.add(proxyCast(toProxy)); + } + try { + set.clear(); + set.addAll(proxies); + return proxies; + } + catch (UnsupportedOperationException ex) { + return Collections.unmodifiableSet(proxies); + } + } + + private Queue proxyQueue(Queue queue) { + Queue proxies = new LinkedList<>(); + for (T toProxy : queue) { + proxies.add(proxyCast(toProxy)); + } + queue.clear(); + queue.addAll(proxies); + return proxies; + } + + private List proxyList(List list) { + List proxies = new ArrayList<>(list.size()); + for (T toProxy : list) { + proxies.add(proxyCast(toProxy)); + } + try { + list.clear(); + list.addAll(proxies); + return proxies; + } + catch (UnsupportedOperationException ex) { + return Collections.unmodifiableList(proxies); + } + } + + private Object[] proxyArray(Object[] objects) { + List retain = new ArrayList<>(objects.length); + for (Object object : objects) { + retain.add(proxy(object)); + } + Object[] proxies = (Object[]) Array.newInstance(objects.getClass().getComponentType(), retain.size()); + for (int i = 0; i < retain.size(); i++) { + proxies[i] = retain.get(i); + } + return proxies; + } + + private SortedMap proxySortedMap(SortedMap entries) { + SortedMap proxies = new TreeMap<>(entries.comparator()); + for (Map.Entry entry : entries.entrySet()) { + proxies.put(entry.getKey(), proxyCast(entry.getValue())); + } + try { + entries.clear(); + entries.putAll(proxies); + return entries; + } + catch (UnsupportedOperationException ex) { + return Collections.unmodifiableSortedMap(proxies); + } + } + + private Map proxyMap(Map entries) { + Map proxies = new LinkedHashMap<>(entries.size()); + for (Map.Entry entry : entries.entrySet()) { + proxies.put(entry.getKey(), proxyCast(entry.getValue())); + } + try { + entries.clear(); + entries.putAll(proxies); + return entries; + } + catch (UnsupportedOperationException ex) { + return Collections.unmodifiableMap(proxies); + } + } + + private Stream proxyStream(Stream stream) { + return stream.map(this::proxy).onClose(stream::close); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private Optional proxyOptional(Optional optional) { + return optional.map(this::proxy); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java new file mode 100644 index 00000000000..0aa4069cb88 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +/** + * A factory for wrapping arbitrary objects in authorization-related advice + * + * @author Josh Cummings + * @since 6.3 + * @see AuthorizationAdvisorProxyFactory + */ +public interface AuthorizationProxyFactory { + + /** + * Wrap the given {@code object} in authorization-related advice. + * + *

+ * Please check the implementation for which kinds of objects it supports. + * @param object the object to proxy + * @return the proxied object + * @throws org.springframework.aop.framework.AopConfigException if a proxy cannot be + * created + */ + Object proxy(Object object); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java new file mode 100644 index 00000000000..deca6b04406 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.core.Ordered; + +/** + * An interface that indicates method security advice + * + * @author Josh Cummings + * @since 6.3 + * @see AuthorizationManagerBeforeMethodInterceptor + * @see AuthorizationManagerAfterMethodInterceptor + * @see PreFilterAuthorizationMethodInterceptor + * @see PostFilterAuthorizationMethodInterceptor + */ +public interface AuthorizationAdvisor extends Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index 2971589e336..4d490515f08 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -25,9 +25,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.core.Ordered; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PostAuthorize; @@ -48,8 +45,7 @@ * @author Josh Cummings * @since 5.6 */ -public final class AuthorizationManagerAfterMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class AuthorizationManagerAfterMethodInterceptor implements AuthorizationAdvisor { private Supplier securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy; diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index 0f38826d13b..4d84a55616d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -28,9 +28,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.core.Ordered; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.annotation.Secured; @@ -52,8 +49,7 @@ * @author Josh Cummings * @since 5.6 */ -public final class AuthorizationManagerBeforeMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class AuthorizationManagerBeforeMethodInterceptor implements AuthorizationAdvisor { private Supplier securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index aadc75c0036..aa96de670da 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.core.Ordered; import org.springframework.expression.EvaluationContext; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PostFilter; @@ -43,8 +40,7 @@ * @author Josh Cummings * @since 5.6 */ -public final class PostFilterAuthorizationMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class PostFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor { private Supplier securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java index 39ae4e257ca..a00e22f2534 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java @@ -23,9 +23,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.core.Ordered; import org.springframework.expression.EvaluationContext; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PreFilter; @@ -44,8 +41,7 @@ * @author Josh Cummings * @since 5.6 */ -public final class PreFilterAuthorizationMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class PreFilterAuthorizationMethodInterceptor implements AuthorizationAdvisor { private Supplier securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy; diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java new file mode 100644 index 00000000000..2a7852b28cb --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java @@ -0,0 +1,431 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Pointcut; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class AuthorizationAdvisorProxyFactoryTests { + + private final Authentication user = TestAuthentication.authenticatedUser(); + + private final Authentication admin = TestAuthentication.authenticatedAdmin(); + + private final Flight flight = new Flight(); + + private final User alan = new User("alan", "alan", "turing"); + + @Test + public void proxyWhenPreAuthorizeThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Flight flight = new Flight(); + assertThat(flight.getAltitude()).isEqualTo(35000d); + Flight secured = proxy(factory, flight); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + assertThat(this.alan.getFirstName()).isEqualTo("alan"); + User secured = proxy(factory, this.alan); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getFirstName); + SecurityContextHolder.getContext().setAuthentication(authenticated("alan")); + assertThat(secured.getFirstName()).isEqualTo("alan"); + SecurityContextHolder.getContext().setAuthentication(this.admin); + assertThat(secured.getFirstName()).isEqualTo("alan"); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeOnRecordThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + HasSecret repo = new Repository("secret"); + assertThat(repo.secret()).isEqualTo("secret"); + HasSecret secured = proxy(factory, repo); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::secret); + SecurityContextHolder.getContext().setAuthentication(this.user); + assertThat(repo.secret()).isEqualTo("secret"); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenImmutableListThenReturnsSecuredImmutableList() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + List flights = List.of(this.flight); + List secured = proxy(factory, flights); + secured.forEach( + (flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude)); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Set flights = Set.of(this.flight); + Set secured = proxy(factory, flights); + secured.forEach( + (flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude)); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenQueueThenReturnsSecuredQueue() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Queue flights = new LinkedList<>(List.of(this.flight)); + Queue secured = proxy(factory, flights); + assertThat(flights.size()).isEqualTo(secured.size()); + secured.forEach( + (flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude)); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + SortedSet users = Collections.unmodifiableSortedSet(new TreeSet<>(Set.of(this.alan))); + SortedSet secured = proxy(factory, users); + secured + .forEach((user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName)); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + SortedMap users = Collections + .unmodifiableSortedMap(new TreeMap<>(Map.of(this.alan.getId(), this.alan))); + SortedMap secured = proxy(factory, users); + secured.forEach( + (id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName)); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Map users = Map.of(this.alan.getId(), this.alan); + Map secured = proxy(factory, users); + secured.forEach( + (id, user) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(user::getFirstName)); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(secured::clear); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenMutableListThenReturnsSecuredMutableList() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + List flights = new ArrayList<>(List.of(this.flight)); + List secured = proxy(factory, flights); + secured.forEach( + (flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude)); + secured.clear(); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenMutableSetThenReturnsSecuredMutableSet() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Set flights = new HashSet<>(Set.of(this.flight)); + Set secured = proxy(factory, flights); + secured.forEach( + (flight) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude)); + secured.clear(); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + SortedSet users = new TreeSet<>(Set.of(this.alan)); + SortedSet secured = proxy(factory, users); + secured.forEach((u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); + secured.clear(); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + SortedMap users = new TreeMap<>(Map.of(this.alan.getId(), this.alan)); + SortedMap secured = proxy(factory, users); + secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); + secured.clear(); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenMutableMapThenReturnsSecuredMutableMap() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Map users = new HashMap<>(Map.of(this.alan.getId(), this.alan)); + Map secured = proxy(factory, users); + secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); + secured.clear(); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForOptionalThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Optional flights = Optional.of(this.flight); + assertThat(flights.get().getAltitude()).isEqualTo(35000d); + Optional secured = proxy(factory, flights); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.ifPresent(Flight::getAltitude)); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForStreamThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Stream flights = Stream.of(this.flight); + Stream secured = proxy(factory, flights); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(Flight::getAltitude)); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForArrayThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Flight[] flights = { this.flight }; + Flight[] secured = proxy(factory, flights); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured[0]::getAltitude); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForIteratorThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Iterator flights = List.of(this.flight).iterator(); + Iterator secured = proxy(factory, flights); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.next().getAltitude()); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForIterableThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Iterable users = new UserRepository(); + Iterable secured = proxy(factory, users); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(User::getFirstName)); + SecurityContextHolder.clearContext(); + } + + @Test + public void proxyWhenPreAuthorizeForClassThenHonors() { + AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor + .preAuthorize(); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + Class clazz = proxy(factory, Flight.class); + assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); + Flight secured = proxy(factory, this.flight); + assertThat(secured.getClass()).isSameAs(clazz); + SecurityContextHolder.getContext().setAuthentication(this.user); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getAltitude); + SecurityContextHolder.clearContext(); + } + + @Test + public void withAdvisorsWhenProxyThenVisits() { + AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class); + given(advisor.getAdvice()).willReturn(advisor); + given(advisor.getPointcut()).willReturn(Pointcut.TRUE); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); + factory = factory.withAdvisors(advisor); + Flight flight = proxy(factory, this.flight); + flight.getAltitude(); + verify(advisor, atLeastOnce()).getPointcut(); + } + + private Authentication authenticated(String user, String... authorities) { + return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build()); + } + + private T proxy(AuthorizationProxyFactory factory, Object target) { + return (T) factory.proxy(target); + } + + static class Flight { + + @PreAuthorize("hasRole('PILOT')") + Double getAltitude() { + return 35000d; + } + + } + + interface Identifiable { + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + String getFirstName(); + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + String getLastName(); + + } + + public static class User implements Identifiable, Comparable { + + private final String id; + + private final String firstName; + + private final String lastName; + + User(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getId() { + return this.id; + } + + @Override + public String getFirstName() { + return this.firstName; + } + + @Override + public String getLastName() { + return this.lastName; + } + + @Override + public int compareTo(@NotNull User that) { + return this.id.compareTo(that.getId()); + } + + } + + static class UserRepository implements Iterable { + + List users = List.of(new User("1", "first", "last")); + + @NotNull + @Override + public Iterator iterator() { + return this.users.iterator(); + } + + } + + interface HasSecret { + + String secret(); + + } + + record Repository(@PreAuthorize("hasRole('ADMIN')") String secret) implements HasSecret { + } + +} diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index ccd328e7d5b..62d956ce2b6 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1702,6 +1702,397 @@ This works on both classes and interfaces. This does not work for interfaces, since they do not have debug information about the parameter names. For interfaces, either annotations or the `-parameters` approach must be used. +[[authorize-object]] +== Authorizing Arbitrary Objects + +Spring Security also supports wrapping any object that is annotated its method security annotations. + +To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured. +If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`. + +For example, consider the following `User` class: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public class User { + private String name; + private String email; + + public User(String name, String email) { + this.name = name; + this.email = email; + } + + public String getName() { + return this.name; + } + + @PreAuthorize("hasAuthority('user:read')") + public String getEmail() { + return this.email; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String) +---- +====== + +You can proxy an instance of user in the following way: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Autowired +AuthorizationProxyFactory proxyFactory; + +@Test +void getEmailWhenProxiedThenAuthorizes() { + User user = new User("name", "email"); + assertThat(user.getEmail()).isNotNull(); + User securedUser = proxyFactory.proxy(user); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Autowired +var proxyFactory:AuthorizationProxyFactory? = null + +@Test +fun getEmailWhenProxiedThenAuthorizes() { + val user: User = User("name", "email") + assertThat(user.getEmail()).isNotNull() + val securedUser: User = proxyFactory.proxy(user) + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail) +} +---- +====== + +=== Manual Construction + +You can also define your own instance if you need something different from the Spring Security default. + +For example, if you define an `AuthorizationProxyFactory` instance like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize; + +// ... + +AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize()); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize + +// ... + +val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize()) +---- +====== + +Then you can wrap any instance of `User` as follows: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Test +void getEmailWhenProxiedThenAuthorizes() { + AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize()); + User user = new User("name", "email"); + assertThat(user.getEmail()).isNotNull(); + User securedUser = proxyFactory.proxy(user); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Test +fun getEmailWhenProxiedThenAuthorizes() { + val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize()) + val user: User = User("name", "email") + assertThat(user.getEmail()).isNotNull() + val securedUser: User = proxyFactory.proxy(user) + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail) +} +---- +====== + +[NOTE] +==== +This feature does not yet support Spring AOT +==== + +=== Proxying Collections + +`AuthorizationProxyFactory` supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type. + +This means that when proxying a `List` of objects, the following also works: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Test +void getEmailWhenProxiedThenAuthorizes() { + AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize()); + List users = List.of(ada, albert, marie); + List securedUsers = proxyFactory.proxy(users); + securedUsers.forEach((securedUser) -> + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail)); +} +---- +====== + +=== Proxying Classes + +In limited circumstances, it may be valuable to proxy a `Class` itself, and `AuthorizationProxyFactory` also supports this. +This is roughly the equivalent of calling `ProxyFactory#getProxyClass` in Spring Framework's support for creating proxies. + +One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT. + +=== Support for All Method Security Annotations + +`AuthorizationProxyFactory` supports whichever method security annotations are enabled in your application. +It is based off of whatever `AuthorizationAdvisor` classes are published as a bean. + +Since `@EnableMethodSecurity` publishes `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` advisors by default, you will typically need to do nothing to activate the ability. + +[NOTE] +==== +SpEL expressions that use `returnObject` or `filterObject` sit behind the proxy and so have full access to the object. +==== + +[#custom_advice] +=== Custom Advice + +If you have security advice that you also want applied, you can publish your own `AuthorizationAdvisor` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@EnableMethodSecurity +class SecurityConfig { + @Bean + static AuthorizationAdvisor myAuthorizationAdvisor() { + return new AuthorizationAdvisor(); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@EnableMethodSecurity +internal class SecurityConfig { + @Bean + fun myAuthorizationAdvisor(): AuthorizationAdvisor { + return AuthorizationAdvisor() + } +] +---- +====== + +And Spring Security will add that advisor into the set of advice that `AuthorizationProxyFactory` adds when proxying an object. + +=== Working with Jackson + +One powerful use of this feature is to return a secured value from a controller like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@RestController +public class UserController { + @Autowired + AuthorizationProxyFactory proxyFactory; + + @GetMapping + User currentUser(@AuthenticationPrincipal User user) { + return this.proxyFactory.proxy(user); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@RestController +class UserController { + @Autowired + var proxyFactory: AuthorizationProxyFactory? = null + + @GetMapping + fun currentUser(@AuthenticationPrincipal user:User?): User { + return proxyFactory.proxy(user) + } +} +---- +====== + +If you are using Jackson, though, this may result in a serialization error like the following: + +[source,bash] +==== +com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle +==== + +This is due to how Jackson works with CGLIB proxies. +To address this, add the following annotation to the top of the `User` class: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@JsonSerialize(as = User.class) +public class User { + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@JsonSerialize(`as` = User::class) +class User +---- +====== + +Finally, you will need to publish a <> to catch the `AccessDeniedException` thrown for each field, which you can do like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Component +public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor { + private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + try { + return invocation.proceed(); + } catch (AccessDeniedException ex) { + return null; + } + } + + @Override + public Pointcut getPointcut() { + return this.advisor.getPointcut(); + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public int getOrder() { + return this.advisor.getOrder() - 1; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +class AccessDeniedExceptionInterceptor: AuthorizationAdvisor { + var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize() + + @Throws(Throwable::class) + fun invoke(invocation: MethodInvocation): Any? { + return try { + invocation.proceed() + } catch (ex:AccessDeniedException) { + null + } + } + + val pointcut: Pointcut + get() = advisor.getPointcut() + + val advice: Advice + get() = this + + val order: Int + get() = advisor.getOrder() - 1 +} +---- +====== + +Then, you'll see a different JSON serialization based on the authorization level of the user. +If they don't have the `user:read` authority, then they'll see: + +[source,json] +---- +{ + "name" : "name", + "email" : null +} +---- + +And if they do have that authority, they'll see: + +[source,json] +---- +{ + "name" : "name", + "email" : "email" +} +---- + +[TIP] +==== +You can also add the Spring Boot property `spring.jackson.default-property-inclusion=non_null` to exclude the null value, if you also don't want to reveal the JSON key to an unauthorized user. +==== + [[migration-enableglobalmethodsecurity]] == Migrating from `@EnableGlobalMethodSecurity` diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 6f1756c0cc0..dc7122d483f 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -8,6 +8,10 @@ Below are the highlights of the release. - https://spring.io/blog/2024/01/19/spring-security-6-3-adds-passive-jdk-serialization-deserialization-for[blog post] - Added Passive JDK Serialization/Deserialization for Seamless Upgrades +== Authorization + +- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security + == Configuration - https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[(docs)] - Add Concurrent Sessions Control on WebFlux From 1465ad59ed0bea200a0dbf104489f43b75bc2a13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:56:34 +0000 Subject: [PATCH 06/31] Bump org.assertj:assertj-core from 3.24.2 to 3.25.3 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.24.2 to 3.25.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.24.2...assertj-build-3.25.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5ba04cf5c2..08d5a1cc1ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,7 @@ org-apereo-cas-client-cas-client-core = "org.apereo.cas.client:cas-client-core:4 io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.4" org-aspectj-aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "org-aspectj" } org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj" } -org-assertj-assertj-core = "org.assertj:assertj-core:3.24.2" +org-assertj-assertj-core = "org.assertj:assertj-core:3.25.3" org-bouncycastle-bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "org-bouncycastle" } org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "org-bouncycastle" } org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" } From b95852f3ae09daeb3b2d875a8a868f46c0c15bd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:56:09 +0000 Subject: [PATCH 07/31] Bump ch.qos.logback:logback-classic from 1.4.14 to 1.5.3 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.14 to 1.5.3. - [Commits](https://github.com/qos-ch/logback/compare/v_1.4.14...v_1.5.3) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08d5a1cc1ea..ba708ef52cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ org-opensaml = "4.3.0" org-springframework = "6.1.4" [libraries] -ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14" +ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.3" com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.15.4" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" From a6abd48aeae39f31e0a09ae15d07bded4e841f97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:56:30 +0000 Subject: [PATCH 08/31] Bump org.hibernate.orm:hibernate-core from 6.3.2.Final to 6.4.4.Final Bumps [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm) from 6.3.2.Final to 6.4.4.Final. - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.4/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.3.2...6.4.4) --- updated-dependencies: - dependency-name: org.hibernate.orm:hibernate-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba708ef52cb..1c3c15b5cd2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", org-eclipse-jetty-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "org-eclipse-jetty" } org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "org-eclipse-jetty" } org-hamcrest = "org.hamcrest:hamcrest:2.2" -org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.3.2.Final" +org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.4.4.Final" org-hsqldb = "org.hsqldb:hsqldb:2.7.2" org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23" From 0c946ba3b3d1b0fa615ce774326fc6070b44f8e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:56:14 +0000 Subject: [PATCH 09/31] Bump org.yaml:snakeyaml from 1.30 to 1.33 Bumps [org.yaml:snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) from 1.30 to 1.33. - [Commits](https://bitbucket.org/snakeyaml/snakeyaml/branches/compare/snakeyaml-1.33..snakeyaml-1.30) --- updated-dependencies: - dependency-name: org.yaml:snakeyaml dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c3c15b5cd2..e7f602f4ac4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipar com-google-code-gson-gson = "com.google.code.gson:gson:2.10.1" com-thaiopensource-trag = "com.thaiopensource:trang:20091111" net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8" -org-yaml-snakeyaml = "org.yaml:snakeyaml:1.30" +org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33" org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2" io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0" org-gretty-gretty = "org.gretty:gretty:4.0.3" From f82e15d5b65734c64856ec955e82f689bf18fe1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 04:03:16 +0000 Subject: [PATCH 10/31] Bump com.fasterxml.jackson:jackson-bom from 2.15.4 to 2.17.0 Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.15.4 to 2.17.0. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.15.4...jackson-bom-2.17.0) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7f602f4ac4..ec008e603d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ org-springframework = "6.1.4" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.3" -com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.15.4" +com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.0" com-google-inject-guice = "com.google.inject:guice:3.0" com-netflix-nebula-nebula-project-plugin = "com.netflix.nebula:nebula-project-plugin:8.2.0" com-nimbusds-nimbus-jose-jwt = "com.nimbusds:nimbus-jose-jwt:9.37.3" From e7bff4240a589bcf80d15c69f8716944a6f73b8e Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Thu, 14 Mar 2024 09:23:31 -0300 Subject: [PATCH 11/31] Use GH_ACTIONS_REPO_TOKEN for merge-dependabot-pr.yml Issue gh-14721 --- .github/workflows/merge-dependabot-pr.yml | 44 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 4244d10bd2e..dfae7104dc7 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -5,9 +5,45 @@ on: run-name: Merge Dependabot PR ${{ github.ref_name }} +env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + jobs: merge-dependabot-pr: - permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@1e8b0587a1f4f01697f9753fa3339c3e0d30f396 - with: - mergeArguments: '--auto --rebase' + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + + - uses: actions/checkout@v4 + with: + show-progress: false + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set Milestone to Dependabot Pull Request + id: set-milestone + run: | + if test -f pom.xml + then + CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout) + else + CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }') + fi + export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT} + MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title') + + if [ -z $MILESTONE ] + then + gh run cancel ${{ github.run_id }} + echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version" + else + gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE + echo mergeEnabled=true >> $GITHUB_OUTPUT + fi + + - name: Merge Dependabot pull request + if: steps.set-milestone.outputs.mergeEnabled + run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase From a87fc4ea8a891a49fc4cb344f2c1f4919eb398fe Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Thu, 14 Mar 2024 14:33:00 -0300 Subject: [PATCH 12/31] Use pull_request_target for merge-dependabot-pr.yml Issue gh-14721 --- .github/workflows/merge-dependabot-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index dfae7104dc7..0dcae6885db 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -1,7 +1,6 @@ name: Merge Dependabot PR -on: - pull_request: +on: pull_request_target run-name: Merge Dependabot PR ${{ github.ref_name }} @@ -17,6 +16,7 @@ jobs: - uses: actions/checkout@v4 with: show-progress: false + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-java@v4 with: From b3a43d615431acf592579e9f13c818b371a56a4f Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Thu, 14 Mar 2024 14:42:33 -0300 Subject: [PATCH 13/31] Use token only for auto merge Issue gh-14721 --- .github/workflows/merge-dependabot-pr.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 0dcae6885db..6331b625095 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -4,8 +4,7 @@ on: pull_request_target run-name: Merge Dependabot PR ${{ github.ref_name }} -env: - GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} +permissions: write-all jobs: merge-dependabot-pr: @@ -47,3 +46,5 @@ jobs: - name: Merge Dependabot pull request if: steps.set-milestone.outputs.mergeEnabled run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} From 37984c2e11e7bbca16b75981b109410cc5577b39 Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Thu, 14 Mar 2024 14:45:21 -0300 Subject: [PATCH 14/31] Add GH_TOKEN to set-milestone Issue gh-14721 --- .github/workflows/merge-dependabot-pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 6331b625095..1bda517c9a1 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -42,6 +42,8 @@ jobs: gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE echo mergeEnabled=true >> $GITHUB_OUTPUT fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Merge Dependabot pull request if: steps.set-milestone.outputs.mergeEnabled From 8e81901c1065b33c02679376e9ea76d6fdbba1e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:46:26 +0000 Subject: [PATCH 15/31] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin Bumps [org.sonarsource.scanner.gradle:sonarqube-gradle-plugin](https://github.com/SonarSource/sonar-scanner-gradle) from 2.7.1 to 2.8.0.1969. - [Release notes](https://github.com/SonarSource/sonar-scanner-gradle/releases) - [Commits](https://github.com/SonarSource/sonar-scanner-gradle/compare/2.7.1...2.8.0.1969) --- updated-dependencies: - dependency-name: org.sonarsource.scanner.gradle:sonarqube-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec008e603d4..504ec8b6d70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,7 +100,7 @@ com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versi com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11" org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1" org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.13" -org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1" +org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969" org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1" [plugins] From d615433dc3c1b62c0289cc4855f008697286653b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:50:31 +0000 Subject: [PATCH 16/31] Bump io.freefair.gradle:aspectj-plugin from 8.4 to 8.6 Bumps [io.freefair.gradle:aspectj-plugin](https://github.com/freefair/gradle-plugins) from 8.4 to 8.6. - [Release notes](https://github.com/freefair/gradle-plugins/releases) - [Commits](https://github.com/freefair/gradle-plugins/compare/8.4...8.6) --- updated-dependencies: - dependency-name: io.freefair.gradle:aspectj-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 504ec8b6d70..1a155fe9ae2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ org-apache-maven-resolver-maven-resolver-connector-basic = { module = "org.apach org-apache-maven-resolver-maven-resolver-impl = { module = "org.apache.maven.resolver:maven-resolver-impl", version.ref = "org-apache-maven-resolver" } org-apache-maven-resolver-maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "org-apache-maven-resolver" } org-apereo-cas-client-cas-client-core = "org.apereo.cas.client:cas-client-core:4.0.4" -io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.4" +io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:8.6" org-aspectj-aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "org-aspectj" } org-aspectj-aspectjweaver = { module = "org.aspectj:aspectjweaver", version.ref = "org-aspectj" } org-assertj-assertj-core = "org.assertj:assertj-core:3.25.3" From be7bfcb278e9d6617b9c07bd09de241922142ac0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:50:22 +0000 Subject: [PATCH 17/31] Bump com.github.ben-manes:gradle-versions-plugin from 0.38.0 to 0.51.0 Bumps [com.github.ben-manes:gradle-versions-plugin](https://github.com/ben-manes/gradle-versions-plugin) from 0.38.0 to 0.51.0. - [Release notes](https://github.com/ben-manes/gradle-versions-plugin/releases) - [Commits](https://github.com/ben-manes/gradle-versions-plugin/compare/v0.38.0...v0.51.0) --- updated-dependencies: - dependency-name: com.github.ben-manes:gradle-versions-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a155fe9ae2..272474a8c03 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -96,7 +96,7 @@ org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33" org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2" io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0" org-gretty-gretty = "org.gretty:gretty:4.0.3" -com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0" +com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0" com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11" org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1" org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.33.13" From 815131938d2fe81d9c32450fde235415610cb04a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:51:01 +0000 Subject: [PATCH 18/31] Bump org.springframework:spring-framework-bom from 6.1.4 to 6.1.5 Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.4 to 6.1.5. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.4...v6.1.5) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 272474a8c03..a7b2eda3e86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.9.23" org-jetbrains-kotlinx = "1.8.0" org-mockito = "5.11.0" org-opensaml = "4.3.0" -org-springframework = "6.1.4" +org-springframework = "6.1.5" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.3" From e6bb866f4bba1326e8577d801804dbcc17b5f7b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:57:49 +0000 Subject: [PATCH 19/31] Bump org.gretty:gretty from 4.0.3 to 4.1.2 Bumps [org.gretty:gretty](https://github.com/gretty-gradle-plugin/gretty) from 4.0.3 to 4.1.2. - [Release notes](https://github.com/gretty-gradle-plugin/gretty/releases) - [Changelog](https://github.com/gretty-gradle-plugin/gretty/blob/master/changes.md) - [Commits](https://github.com/gretty-gradle-plugin/gretty/compare/v4.0.3...v4.1.2) --- updated-dependencies: - dependency-name: org.gretty:gretty dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7b2eda3e86..b349e0ea52b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,7 +95,7 @@ net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8" org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33" org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2" io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0" -org-gretty-gretty = "org.gretty:gretty:4.0.3" +org-gretty-gretty = "org.gretty:gretty:4.1.2" com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0" com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11" org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1" From fdbfc58d548fe62d08d44e83090ee57cc46667fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:59:04 +0000 Subject: [PATCH 20/31] Bump org.springframework:spring-framework-bom from 6.1.4 to 6.1.5 Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.4 to 6.1.5. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.4...v6.1.5) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2fe42b6e1d0..d575d080168 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.9.23" org-jetbrains-kotlinx = "1.7.3" org-mockito = "5.5.0" org-opensaml = "4.3.0" -org-springframework = "6.1.4" +org-springframework = "6.1.5" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14" From a6d362fa18e87636108d2adbf2268bc158e844e3 Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Thu, 14 Mar 2024 15:12:14 -0300 Subject: [PATCH 21/31] Make trigger-dependabot-auto-merge-forward.yml run on push Issue gh-14721 --- .../workflows/trigger-dependabot-auto-merge-forward.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trigger-dependabot-auto-merge-forward.yml b/.github/workflows/trigger-dependabot-auto-merge-forward.yml index 0f1bf228c4e..9bbb2ccbd5f 100644 --- a/.github/workflows/trigger-dependabot-auto-merge-forward.yml +++ b/.github/workflows/trigger-dependabot-auto-merge-forward.yml @@ -1,8 +1,9 @@ name: Trigger Dependabot Auto Merge Forward on: - pull_request: - types: [closed] + push: + branches: + - '*.x' permissions: read-all @@ -10,7 +11,7 @@ jobs: trigger-worflow: name: Trigger Workflow runs-on: ubuntu-latest - if: ${{ github.event.pull_request.merged == true && github.actor == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }} + if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }} steps: - name: Checkout id: checkout From 80a8d3831a1102a5b118a5d4602a3ce34a5b1228 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:19:32 -0500 Subject: [PATCH 22/31] Simplify reactive OAuth2 Client configuration Closes gh-13763 --- .../ReactiveOAuth2ClientConfiguration.java | 413 ++++++++++++ .../ReactiveOAuth2ClientImportSelector.java | 101 +-- ...orizedClientManagerConfigurationTests.java | 589 ++++++++++++++++++ 3 files changed, 1007 insertions(+), 96 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2AuthorizedClientManagerConfigurationTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java new file mode 100644 index 00000000000..56e25bd70c9 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientConfiguration.java @@ -0,0 +1,413 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.ResolvableType; +import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.DelegatingReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; +import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; + +/** + * {@link Configuration} for OAuth 2.0 Client support. + * + *

+ * This {@code Configuration} is conditionally imported by + * {@link ReactiveOAuth2ClientImportSelector} when the + * {@code spring-security-oauth2-client} module is present on the classpath. + * + * @author Steve Riesenberg + * @since 6.3 + * @see ReactiveOAuth2ClientImportSelector + */ +@Import({ ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration.class, + ReactiveOAuth2ClientConfiguration.OAuth2ClientWebFluxSecurityConfiguration.class }) +final class ReactiveOAuth2ClientConfiguration { + + @Configuration + static class ReactiveOAuth2AuthorizedClientManagerConfiguration { + + @Bean(name = ReactiveOAuth2AuthorizedClientManagerRegistrar.BEAN_NAME) + ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() { + return new ReactiveOAuth2AuthorizedClientManagerRegistrar(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { + + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + + private ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + + @Override + public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { + ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager(); + if (authorizedClientManager != null) { + configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager)); + } + } + + @Autowired(required = false) + void setAuthorizedClientManager(List authorizedClientManager) { + if (authorizedClientManager.size() == 1) { + this.authorizedClientManager = authorizedClientManager.get(0); + } + } + + @Autowired + void setAuthorizedClientManagerRegistrar( + ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { + this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; + } + + private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() { + if (this.authorizedClientManager != null) { + return this.authorizedClientManager; + } + return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable(); + } + + } + + /** + * A registrar for registering the default + * {@link ReactiveOAuth2AuthorizedClientManager} bean definition, if not already + * present. + */ + static final class ReactiveOAuth2AuthorizedClientManagerRegistrar + implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { + + static final String BEAN_NAME = "authorizedClientManagerRegistrar"; + + static final String FACTORY_METHOD_NAME = "getAuthorizedClientManager"; + + // @formatter:off + private static final Set> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of( + AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class, + RefreshTokenReactiveOAuth2AuthorizedClientProvider.class, + ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class, + PasswordReactiveOAuth2AuthorizedClientProvider.class, + JwtBearerReactiveOAuth2AuthorizedClientProvider.class, + TokenExchangeReactiveOAuth2AuthorizedClientProvider.class + ); + // @formatter:on + + private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); + + private ListableBeanFactory beanFactory; + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + if (getBeanNamesForType(ReactiveOAuth2AuthorizedClientManager.class).length != 0 + || getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1 + || getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1 + && getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) { + return; + } + + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(ReactiveOAuth2AuthorizedClientManager.class) + .setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME) + .getBeanDefinition(); + + registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry), + beanDefinition); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() { + if (getBeanNamesForType(ReactiveClientRegistrationRepository.class).length != 1 + || getBeanNamesForType(ServerOAuth2AuthorizedClientRepository.class).length != 1 + && getBeanNamesForType(ReactiveOAuth2AuthorizedClientService.class).length != 1) { + return null; + } + return getAuthorizedClientManager(); + } + + ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() { + ReactiveClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils + .beanOfTypeIncludingAncestors(this.beanFactory, ReactiveClientRegistrationRepository.class, true, true); + + ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + try { + authorizedClientRepository = BeanFactoryUtils.beanOfTypeIncludingAncestors(this.beanFactory, + ServerOAuth2AuthorizedClientRepository.class, true, true); + } + catch (NoSuchBeanDefinitionException ex) { + ReactiveOAuth2AuthorizedClientService authorizedClientService = BeanFactoryUtils + .beanOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientService.class, true, + true); + authorizedClientRepository = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository( + authorizedClientService); + } + + Collection authorizedClientProviderBeans = BeanFactoryUtils + .beansOfTypeIncludingAncestors(this.beanFactory, ReactiveOAuth2AuthorizedClientProvider.class, true, + true) + .values(); + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider; + if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) { + authorizedClientProvider = authorizedClientProviderBeans.iterator().next(); + } + else { + List authorizedClientProviders = new ArrayList<>(); + authorizedClientProviders + .add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans)); + authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans)); + authorizedClientProviders + .add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans)); + authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans)); + + ReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider( + authorizedClientProviderBeans); + if (jwtBearerAuthorizedClientProvider != null) { + authorizedClientProviders.add(jwtBearerAuthorizedClientProvider); + } + + ReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider = getTokenExchangeAuthorizedClientProvider( + authorizedClientProviderBeans); + if (tokenExchangeAuthorizedClientProvider != null) { + authorizedClientProviders.add(tokenExchangeAuthorizedClientProvider); + } + + authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans)); + authorizedClientProvider = new DelegatingReactiveOAuth2AuthorizedClientProvider( + authorizedClientProviders); + } + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + Consumer authorizedClientManagerConsumer = getBeanOfType( + ResolvableType.forClassWithGenerics(Consumer.class, + DefaultReactiveOAuth2AuthorizedClientManager.class)); + if (authorizedClientManagerConsumer != null) { + authorizedClientManagerConsumer.accept(authorizedClientManager); + } + + return authorizedClientManager; + } + + private boolean hasDelegatingAuthorizedClientProvider( + Collection authorizedClientProviders) { + if (authorizedClientProviders.size() != 1) { + return false; + } + return authorizedClientProviders.iterator() + .next() instanceof DelegatingReactiveOAuth2AuthorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider( + Collection authorizedClientProviders) { + AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class); + if (authorizedClientProvider == null) { + authorizedClientProvider = new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider(); + } + + return authorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider( + Collection authorizedClientProviders) { + RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, RefreshTokenReactiveOAuth2AuthorizedClientProvider.class); + if (authorizedClientProvider == null) { + authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider(); + } + + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( + ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class, + OAuth2RefreshTokenGrantRequest.class)); + if (accessTokenResponseClient != null) { + authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); + } + + return authorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider( + Collection authorizedClientProviders) { + ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class); + if (authorizedClientProvider == null) { + authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider(); + } + + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( + ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class, + OAuth2ClientCredentialsGrantRequest.class)); + if (accessTokenResponseClient != null) { + authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); + } + + return authorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider( + Collection authorizedClientProviders) { + PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, PasswordReactiveOAuth2AuthorizedClientProvider.class); + if (authorizedClientProvider == null) { + authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider(); + } + + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( + ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class, + OAuth2PasswordGrantRequest.class)); + if (accessTokenResponseClient != null) { + authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); + } + + return authorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider( + Collection authorizedClientProviders) { + JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, JwtBearerReactiveOAuth2AuthorizedClientProvider.class); + + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( + ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class, + JwtBearerGrantRequest.class)); + if (accessTokenResponseClient != null) { + if (authorizedClientProvider == null) { + authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider(); + } + + authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); + } + + return authorizedClientProvider; + } + + private ReactiveOAuth2AuthorizedClientProvider getTokenExchangeAuthorizedClientProvider( + Collection authorizedClientProviders) { + TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( + authorizedClientProviders, TokenExchangeReactiveOAuth2AuthorizedClientProvider.class); + + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( + ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class, + TokenExchangeGrantRequest.class)); + if (accessTokenResponseClient != null) { + if (authorizedClientProvider == null) { + authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider(); + } + + authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); + } + + return authorizedClientProvider; + } + + private List getAdditionalAuthorizedClientProviders( + Collection authorizedClientProviders) { + List additionalAuthorizedClientProviders = new ArrayList<>( + authorizedClientProviders); + additionalAuthorizedClientProviders + .removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass())); + return additionalAuthorizedClientProviders; + } + + private T getAuthorizedClientProviderByType( + Collection authorizedClientProviders, Class providerClass) { + T authorizedClientProvider = null; + for (ReactiveOAuth2AuthorizedClientProvider current : authorizedClientProviders) { + if (providerClass.isInstance(current)) { + assertAuthorizedClientProviderIsNull(authorizedClientProvider); + authorizedClientProvider = providerClass.cast(current); + } + } + return authorizedClientProvider; + } + + private static void assertAuthorizedClientProviderIsNull( + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) { + if (authorizedClientProvider != null) { + // @formatter:off + throw new BeanInitializationException(String.format( + "Unable to create a %s bean. Expected one bean of type %s, but found multiple. " + + "Please consider defining only a single bean of this type, or define a %s bean yourself.", + ReactiveOAuth2AuthorizedClientManager.class.getName(), + authorizedClientProvider.getClass().getName(), + ReactiveOAuth2AuthorizedClientManager.class.getName())); + // @formatter:on + } + } + + private String[] getBeanNamesForType(Class beanClass) { + return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, true, true); + } + + private T getBeanOfType(ResolvableType resolvableType) { + ObjectProvider objectProvider = this.beanFactory.getBeanProvider(resolvableType, true); + return objectProvider.getIfAvailable(); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java index 9a1781b93bf..5e73fb7c894 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -16,27 +16,13 @@ package org.springframework.security.config.annotation.web.reactive; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; -import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.util.ClassUtils; -import org.springframework.web.reactive.config.WebFluxConfigurer; -import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; /** - * {@link Configuration} for OAuth 2.0 Client support. + * Used by {@link EnableWebFluxSecurity} to conditionally import + * {@link ReactiveOAuth2ClientConfiguration}. * *

* This {@code Configuration} is imported by {@link EnableWebFluxSecurity} @@ -60,85 +46,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { if (!oauth2ClientPresent) { return new String[0]; } - return new String[] { "org.springframework.security.config.annotation.web.reactive." - + "ReactiveOAuth2ClientImportSelector$OAuth2ClientWebFluxSecurityConfiguration" }; - } - - @Configuration(proxyBeanMethods = false) - static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { - - private ReactiveClientRegistrationRepository clientRegistrationRepository; - - private ServerOAuth2AuthorizedClientRepository authorizedClientRepository; - - private ReactiveOAuth2AuthorizedClientService authorizedClientService; - - private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; - - @Override - public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - ReactiveOAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager(); - if (authorizedClientManager != null) { - configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager)); - } - } - - @Autowired(required = false) - void setClientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) { - this.clientRegistrationRepository = clientRegistrationRepository; - } - - @Autowired(required = false) - void setAuthorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - this.authorizedClientRepository = authorizedClientRepository; - } - - @Autowired(required = false) - void setAuthorizedClientService(List authorizedClientService) { - if (authorizedClientService.size() == 1) { - this.authorizedClientService = authorizedClientService.get(0); - } - } - - @Autowired(required = false) - void setAuthorizedClientManager(List authorizedClientManager) { - if (authorizedClientManager.size() == 1) { - this.authorizedClientManager = authorizedClientManager.get(0); - } - } - - private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() { - if (this.authorizedClientRepository != null) { - return this.authorizedClientRepository; - } - if (this.authorizedClientService != null) { - return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(this.authorizedClientService); - } - return null; - } - - private ReactiveOAuth2AuthorizedClientManager getAuthorizedClientManager() { - if (this.authorizedClientManager != null) { - return this.authorizedClientManager; - } - ReactiveOAuth2AuthorizedClientManager authorizedClientManager = null; - if (this.authorizedClientRepository != null && this.clientRegistrationRepository != null) { - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder - .builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( - this.clientRegistrationRepository, getAuthorizedClientRepository()); - defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - authorizedClientManager = defaultReactiveOAuth2AuthorizedClientManager; - } - - return authorizedClientManager; - } - + return new String[] { + "org.springframework.security.config.annotation.web.reactive.ReactiveOAuth2ClientConfiguration" }; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2AuthorizedClientManagerConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2AuthorizedClientManagerConfigurationTests.java new file mode 100644 index 00000000000..dd7698e98db --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2AuthorizedClientManagerConfigurationTests.java @@ -0,0 +1,589 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.oauth2.client.AuthorizationCodeReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.JwtBearerReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.PasswordReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.TokenExchangeReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; +import org.springframework.security.oauth2.jwt.JoseHeaderNames; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for + * {@link ReactiveOAuth2ClientConfiguration.ReactiveOAuth2AuthorizedClientManagerConfiguration}. + * + * @author Steve Riesenberg + */ +public class ReactiveOAuth2AuthorizedClientManagerConfigurationTests { + + private static ReactiveOAuth2AccessTokenResponseClient MOCK_RESPONSE_CLIENT; + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Autowired(required = false) + private ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + + @Autowired(required = false) + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @Autowired(required = false) + private AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider; + + private MockServerWebExchange exchange; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + MOCK_RESPONSE_CLIENT = mock(ReactiveOAuth2AccessTokenResponseClient.class); + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + this.exchange = MockServerWebExchange.builder(request).build(); + } + + @Test + public void loadContextWhenOAuth2ClientEnabledThenConfigured() { + this.spring.register(MinimalOAuth2ClientConfig.class).autowire(); + assertThat(this.authorizedClientManager).isNotNull(); + } + + @Test + public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER"); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId("google") + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + assertThatExceptionOfType(ClientAuthorizationRequiredException.class) + .isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block()) + .extracting(OAuth2AuthorizationException::getError) + .extracting(OAuth2Error::getErrorCode) + .isEqualTo("client_authorization_required"); + // @formatter:on + + verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class)); + } + + @Test + public void authorizeWhenAuthorizedClientServiceBeanThenUsed() { + this.spring.register(CustomAuthorizedClientServiceConfig.class).autowire(); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER"); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId("google") + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + assertThatExceptionOfType(ClientAuthorizationRequiredException.class) + .isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block()) + .extracting(OAuth2AuthorizationException::getError) + .extracting(OAuth2Error::getErrorCode) + .isEqualTo("client_authorization_required"); + // @formatter:on + + verify(this.authorizedClientService).loadAuthorizedClient(authorizeRequest.getClientRegistrationId(), + authentication.getName()); + } + + @Test + public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() { + this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); + testRefreshTokenGrant(); + } + + @Test + public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + testRefreshTokenGrant(); + } + + private void testRefreshTokenGrant() { + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER"); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google") + .block(); + assertThat(clientRegistration).isNotNull(); + OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration, + authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken()); + this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.exchange) + .block(); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistration.getRegistrationId()) + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block(); + assertThat(authorizedClient).isNotNull(); + + ArgumentCaptor grantRequestCaptor = ArgumentCaptor + .forClass(OAuth2RefreshTokenGrantRequest.class); + verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); + + OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue(); + assertThat(grantRequest.getClientRegistration().getRegistrationId()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); + assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken()); + assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken()); + } + + @Test + public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() { + this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); + testClientCredentialsGrant(); + } + + @Test + public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + testClientCredentialsGrant(); + } + + private void testClientCredentialsGrant() { + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null, "ROLE_USER"); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github") + .block(); + assertThat(clientRegistration).isNotNull(); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistration.getRegistrationId()) + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block(); + assertThat(authorizedClient).isNotNull(); + + ArgumentCaptor grantRequestCaptor = ArgumentCaptor + .forClass(OAuth2ClientCredentialsGrantRequest.class); + verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); + + OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue(); + assertThat(grantRequest.getClientRegistration().getRegistrationId()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); + } + + @Test + public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() { + this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); + testPasswordGrant(); + } + + @Test + public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + testPasswordGrant(); + } + + private void testPasswordGrant() { + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook") + .block(); + assertThat(clientRegistration).isNotNull(); + MockServerHttpRequest request = MockServerHttpRequest.post("/") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body("username=user&password=password"); + this.exchange = MockServerWebExchange.builder(request).build(); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistration.getRegistrationId()) + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block(); + assertThat(authorizedClient).isNotNull(); + + ArgumentCaptor grantRequestCaptor = ArgumentCaptor + .forClass(OAuth2PasswordGrantRequest.class); + verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); + + OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue(); + assertThat(grantRequest.getClientRegistration().getRegistrationId()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD); + assertThat(grantRequest.getUsername()).isEqualTo("user"); + assertThat(grantRequest.getPassword()).isEqualTo("password"); + } + + @Test + public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() { + this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); + testJwtBearerGrant(); + } + + @Test + public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + testJwtBearerGrant(); + } + + private void testJwtBearerGrant() { + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + + JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt()); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta").block(); + assertThat(clientRegistration).isNotNull(); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistration.getRegistrationId()) + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block(); + assertThat(authorizedClient).isNotNull(); + + ArgumentCaptor grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class); + verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); + + JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue(); + assertThat(grantRequest.getClientRegistration().getRegistrationId()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER); + assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user"); + } + + @Test + public void authorizeWhenTokenExchangeAccessTokenResponseClientBeanThenUsed() { + this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); + testTokenExchangeGrant(); + } + + @Test + public void authorizeWhenTokenExchangeAuthorizedClientProviderBeanThenUsed() { + this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); + testTokenExchangeGrant(); + } + + private void testTokenExchangeGrant() { + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(TokenExchangeGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + + JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt()); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("auth0").block(); + assertThat(clientRegistration).isNotNull(); + // @formatter:off + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistration.getRegistrationId()) + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), this.exchange) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block(); + assertThat(authorizedClient).isNotNull(); + + ArgumentCaptor grantRequestCaptor = ArgumentCaptor + .forClass(TokenExchangeGrantRequest.class); + verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); + + TokenExchangeGrantRequest grantRequest = grantRequestCaptor.getValue(); + assertThat(grantRequest.getClientRegistration().getRegistrationId()) + .isEqualTo(clientRegistration.getRegistrationId()); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE); + assertThat(grantRequest.getSubjectToken()).isEqualTo(authentication.getToken()); + } + + private static OAuth2AccessToken getExpiredAccessToken() { + Instant expiresAt = Instant.now().minusSeconds(60); + Instant issuedAt = expiresAt.minus(Duration.ofDays(1)); + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt, + new HashSet<>(Arrays.asList("read", "write"))); + } + + private static Jwt getJwt() { + Instant issuedAt = Instant.now(); + return new Jwt("token", issuedAt, issuedAt.plusSeconds(300), + Collections.singletonMap(JoseHeaderNames.ALG, "RS256"), + Collections.singletonMap(JwtClaimNames.SUB, "user")); + } + + @Configuration + @EnableWebFluxSecurity + static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig { + + @Bean + ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { + return new WebSessionServerOAuth2AuthorizedClientRepository(); + } + + } + + @Configuration + @EnableWebFluxSecurity + static class CustomAuthorizedClientServiceConfig extends OAuth2ClientBaseConfig { + + @Bean + ReactiveOAuth2AuthorizedClientService authorizedClientService() { + ReactiveOAuth2AuthorizedClientService authorizedClientService = mock( + ReactiveOAuth2AuthorizedClientService.class); + given(authorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(Mono.empty()); + return authorizedClientService; + } + + } + + @Configuration + @EnableWebFluxSecurity + static class CustomAccessTokenResponseClientsConfig extends MinimalOAuth2ClientConfig { + + @Bean + ReactiveOAuth2AccessTokenResponseClient authorizationCodeAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient refreshTokenTokenAccessResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient clientCredentialsAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient passwordAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient jwtBearerAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + @Bean + ReactiveOAuth2AccessTokenResponseClient tokenExchangeAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); + } + + } + + @Configuration + @EnableWebFluxSecurity + static class CustomAuthorizedClientProvidersConfig extends MinimalOAuth2ClientConfig { + + @Bean + AuthorizationCodeReactiveOAuth2AuthorizedClientProvider authorizationCode() { + return spy(new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider()); + } + + @Bean + RefreshTokenReactiveOAuth2AuthorizedClientProvider refreshToken() { + RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider(); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); + return authorizedClientProvider; + } + + @Bean + ClientCredentialsReactiveOAuth2AuthorizedClientProvider clientCredentials() { + ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider(); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); + return authorizedClientProvider; + } + + @Bean + PasswordReactiveOAuth2AuthorizedClientProvider password() { + PasswordReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordReactiveOAuth2AuthorizedClientProvider(); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); + return authorizedClientProvider; + } + + @Bean + JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearer() { + JwtBearerReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider(); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); + return authorizedClientProvider; + } + + @Bean + TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchange() { + TokenExchangeReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeReactiveOAuth2AuthorizedClientProvider(); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); + return authorizedClientProvider; + } + + } + + abstract static class OAuth2ClientBaseConfig { + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository() { + // @formatter:off + return new InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .build(), + CommonOAuth2Provider.GITHUB.getBuilder("github") + .clientId("github-client-id") + .clientSecret("github-client-secret") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(), + CommonOAuth2Provider.FACEBOOK.getBuilder("facebook") + .clientId("facebook-client-id") + .clientSecret("facebook-client-secret") + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .build(), + CommonOAuth2Provider.OKTA.getBuilder("okta") + .clientId("okta-client-id") + .clientSecret("okta-client-secret") + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .build(), + ClientRegistration.withRegistrationId("auth0") + .clientName("Auth0") + .clientId("auth0-client-id") + .clientSecret("auth0-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE) + .scope("user.read", "user.write") + .build()); + // @formatter:on + } + + @Bean + Consumer authorizedClientManagerConsumer() { + return (authorizedClientManager) -> authorizedClientManager + .setContextAttributesMapper((authorizeRequest) -> { + ServerWebExchange exchange = Objects + .requireNonNull(authorizeRequest.getAttribute(ServerWebExchange.class.getName())); + return exchange.getFormData().map((parameters) -> { + String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); + String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); + + Map attributes = Collections.emptyMap(); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + attributes = new HashMap<>(); + attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + + return attributes; + }); + }); + + } + + } + + private static class MockAccessTokenResponseClient + implements ReactiveOAuth2AccessTokenResponseClient { + + @Override + public Mono getTokenResponse(T grantRequest) { + return MOCK_RESPONSE_CLIENT.getTokenResponse(grantRequest); + } + + } + +} From 77c30c431e2cf6bb04bf30b4e34db1d5f2ed1107 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:40:22 -0500 Subject: [PATCH 23/31] Polish tests Issue gh-11783 Issue gh-13763 --- ...orizedClientManagerConfigurationTests.java | 114 +++--------------- ...AuthorizedClientManagerRegistrarTests.java | 82 +++---------- ...dClientManagerRegistrarTests-providers.xml | 12 +- 3 files changed, 39 insertions(+), 169 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java index 4c1fd5f2daf..7e58ce5b8e4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -38,8 +38,6 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; @@ -61,14 +59,9 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -80,13 +73,10 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -397,42 +387,32 @@ static class CustomAccessTokenResponseClientsConfig extends OAuth2ClientBaseConf @Bean OAuth2AccessTokenResponseClient authorizationCodeTokenResponseClient() { - return new MockAuthorizationCodeClient(); + return new MockAccessTokenResponseClient<>(); } @Bean OAuth2AccessTokenResponseClient refreshTokenTokenResponseClient() { - return new MockRefreshTokenClient(); + return new MockAccessTokenResponseClient<>(); } @Bean OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient() { - return new MockClientCredentialsClient(); + return new MockAccessTokenResponseClient<>(); } @Bean OAuth2AccessTokenResponseClient passwordTokenResponseClient() { - return new MockPasswordClient(); + return new MockAccessTokenResponseClient<>(); } @Bean OAuth2AccessTokenResponseClient jwtBearerTokenResponseClient() { - return new MockJwtBearerClient(); + return new MockAccessTokenResponseClient<>(); } @Bean OAuth2AccessTokenResponseClient tokenExchangeTokenResponseClient() { - return new MockTokenExchangeClient(); - } - - @Bean - OAuth2UserService oauth2UserService() { - return mock(DefaultOAuth2UserService.class); - } - - @Bean - OAuth2UserService oidcUserService() { - return mock(OidcUserService.class); + return new MockAccessTokenResponseClient<>(); } } @@ -449,35 +429,35 @@ AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeProvider() { @Bean RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() { RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient()); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); return authorizedClientProvider; } @Bean ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() { ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient()); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); return authorizedClientProvider; } @Bean PasswordOAuth2AuthorizedClientProvider passwordProvider() { PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient()); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); return authorizedClientProvider; } @Bean JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() { JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient()); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); return authorizedClientProvider; } @Bean TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider() { TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockTokenExchangeClient()); + authorizedClientProvider.setAccessTokenResponseClient(new MockAccessTokenResponseClient<>()); return authorizedClientProvider; } @@ -485,21 +465,10 @@ TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvide abstract static class OAuth2ClientBaseConfig { - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login(Customizer.withDefaults()) - .oauth2Client(Customizer.withDefaults()); - return http.build(); - // @formatter:on - } - @Bean ClientRegistrationRepository clientRegistrationRepository() { // @formatter:off - return new InMemoryClientRegistrationRepository(Arrays.asList( + return new InMemoryClientRegistrationRepository( CommonOAuth2Provider.GOOGLE.getBuilder("google") .clientId("google-client-id") .clientSecret("google-client-secret") @@ -527,7 +496,7 @@ ClientRegistrationRepository clientRegistrationRepository() { .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE) .scope("user.read", "user.write") - .build())); + .build()); // @formatter:on } @@ -558,60 +527,11 @@ Consumer authorizedClientManagerConsumer() } - private static class MockAuthorizationCodeClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockRefreshTokenClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockClientCredentialsClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockPasswordClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockTokenExchangeClient implements OAuth2AccessTokenResponseClient { + private static class MockAccessTokenResponseClient + implements OAuth2AccessTokenResponseClient { @Override - public OAuth2AccessTokenResponse getTokenResponse(TokenExchangeGrantRequest authorizationGrantRequest) { + public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) { return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); } diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java index e80444609a1..adeb9d8cb07 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -53,7 +53,6 @@ import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; @@ -430,114 +429,65 @@ public static Consumer authorizedClientMan }); } - public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider() { + public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCode() { return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider()); } - public static RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider() { + public static RefreshTokenOAuth2AuthorizedClientProvider refreshToken() { RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(refreshTokenAccessTokenResponseClient()); return authorizedClientProvider; } - public static MockRefreshTokenClient refreshTokenAccessTokenResponseClient() { - return new MockRefreshTokenClient(); + public static OAuth2AccessTokenResponseClient refreshTokenAccessTokenResponseClient() { + return new MockAccessTokenResponseClient<>(); } - public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() { + public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentials() { ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(clientCredentialsAccessTokenResponseClient()); return authorizedClientProvider; } public static OAuth2AccessTokenResponseClient clientCredentialsAccessTokenResponseClient() { - return new MockClientCredentialsClient(); + return new MockAccessTokenResponseClient<>(); } - public static PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider() { + public static PasswordOAuth2AuthorizedClientProvider password() { PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(passwordAccessTokenResponseClient()); return authorizedClientProvider; } public static OAuth2AccessTokenResponseClient passwordAccessTokenResponseClient() { - return new MockPasswordClient(); + return new MockAccessTokenResponseClient<>(); } - public static JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() { + public static JwtBearerOAuth2AuthorizedClientProvider jwtBearer() { JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient()); return authorizedClientProvider; } public static OAuth2AccessTokenResponseClient jwtBearerAccessTokenResponseClient() { - return new MockJwtBearerClient(); + return new MockAccessTokenResponseClient<>(); } - public static TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider() { + public static TokenExchangeOAuth2AuthorizedClientProvider tokenExchange() { TokenExchangeOAuth2AuthorizedClientProvider authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider(); authorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient()); return authorizedClientProvider; } public static OAuth2AccessTokenResponseClient tokenExchangeAccessTokenResponseClient() { - return new MockTokenExchangeClient(); + return new MockAccessTokenResponseClient<>(); } - private static class MockAuthorizationCodeClient - implements OAuth2AccessTokenResponseClient { + private static class MockAccessTokenResponseClient + implements OAuth2AccessTokenResponseClient { @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockRefreshTokenClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockClientCredentialsClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockPasswordClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockTokenExchangeClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(TokenExchangeGrantRequest authorizationGrantRequest) { + public OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest) { return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); } diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml index 5241e5edb79..0f167f5ed1a 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml @@ -42,21 +42,21 @@ factory-method="authorizedClientManagerConsumer"/> + factory-method="authorizationCode"/> + factory-method="refreshToken"/> + factory-method="clientCredentials"/> + factory-method="password"/> + factory-method="jwtBearer"/> + factory-method="tokenExchange"/> \ No newline at end of file From 05ce92b173a8be75097f443fe2223ff7dd588eab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 03:55:53 +0000 Subject: [PATCH 24/31] Bump io.github.gradle-nexus:publish-plugin from 1.1.0 to 1.3.0 Bumps [io.github.gradle-nexus:publish-plugin](https://github.com/gradle-nexus/publish-plugin) from 1.1.0 to 1.3.0. - [Release notes](https://github.com/gradle-nexus/publish-plugin/releases) - [Commits](https://github.com/gradle-nexus/publish-plugin/compare/v1.1.0...v1.3.0) --- updated-dependencies: - dependency-name: io.github.gradle-nexus:publish-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b349e0ea52b..39caea6075b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ com-thaiopensource-trag = "com.thaiopensource:trang:20091111" net-sourceforge-saxon-saxon = "net.sourceforge.saxon:saxon:9.1.0.8" org-yaml-snakeyaml = "org.yaml:snakeyaml:1.33" org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2" -io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0" +io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.3.0" org-gretty-gretty = "org.gretty:gretty:4.1.2" com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.51.0" com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11" From 9ab5022948d8edc1b017d476a58d747297a08842 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 03:55:44 +0000 Subject: [PATCH 25/31] Bump com.gradle.enterprise from 3.12.6 to 3.16.2 Bumps com.gradle.enterprise from 3.12.6 to 3.16.2. --- updated-dependencies: - dependency-name: com.gradle.enterprise dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 3e0093ad53b..6ec23e53d04 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.12.6" + id "com.gradle.enterprise" version "3.16.2" id "io.spring.ge.conventions" version "0.0.15" } From 3b355a027870f0fc737ebba38cba48e5055c22a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 03:54:08 +0000 Subject: [PATCH 26/31] Bump org.springframework:spring-framework-bom from 5.3.32 to 5.3.33 Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 5.3.32 to 5.3.33. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v5.3.32...v5.3.33) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f25ec87b9e0..0c49b2cc1a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ org-mockito = "4.8.1" org-opensaml4 = "4.1.0" org-opensaml3 = "3.4.6" org-slf4j = "1.7.36" -org-springframework = "5.3.32" +org-springframework = "5.3.33" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13" From 8d6ede27d1c4f40a6586fa950a61eee30811b903 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 03:58:39 +0000 Subject: [PATCH 27/31] Bump org.springframework:spring-framework-bom from 6.0.17 to 6.0.18 Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.0.17 to 6.0.18. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.0.17...v6.0.18) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 35ba18d76ff..74d156d5d48 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ org-jetbrains-kotlin = "1.8.22" org-jetbrains-kotlinx = "1.6.4" org-mockito = "4.8.1" org-opensaml = "4.1.1" -org-springframework = "6.0.17" +org-springframework = "6.0.18" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.4.14" From f541bce49278c082abfa3991444ce29dbafedb45 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 15 Mar 2024 11:27:36 -0600 Subject: [PATCH 28/31] Polish AuthorizationAdvisorProxyFactory - Ensure Reasonable Defaults - Simplify Construction Issue gh-14596 --- .../AuthorizationProxyConfiguration.java | 6 +- .../AuthorizationAdvisorProxyFactory.java | 60 +++++++------ ...AuthorizationAdvisorProxyFactoryTests.java | 85 +++++-------------- 3 files changed, 61 insertions(+), 90 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java index 6aa247c6670..9b561eb221e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -25,7 +25,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisor; @@ -37,8 +36,9 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean { static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider provider) { List advisors = new ArrayList<>(); provider.forEach(advisors::add); - AnnotationAwareOrderComparator.sort(advisors); - return new AuthorizationAdvisorProxyFactory(advisors); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); + factory.setAdvisors(advisors); + return factory; } } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java index 3fbb8df980f..ceffb7ded11 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java @@ -40,6 +40,10 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; +import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.util.ClassUtils; /** @@ -71,31 +75,15 @@ */ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory { - private final Collection advisors; + private List advisors = new ArrayList<>(); - public AuthorizationAdvisorProxyFactory(AuthorizationAdvisor... advisors) { - this.advisors = List.of(advisors); - } - - public AuthorizationAdvisorProxyFactory(Collection advisors) { - this.advisors = List.copyOf(advisors); - } - - /** - * Create a new {@link AuthorizationAdvisorProxyFactory} that includes the given - * advisors in addition to any advisors {@code this} instance already has. - * - *

- * All advisors are re-sorted by their advisor order. - * @param advisors the advisors to add - * @return a new {@link AuthorizationAdvisorProxyFactory} instance - */ - public AuthorizationAdvisorProxyFactory withAdvisors(AuthorizationAdvisor... advisors) { - List merged = new ArrayList<>(this.advisors.size() + advisors.length); - merged.addAll(this.advisors); - merged.addAll(List.of(advisors)); - AnnotationAwareOrderComparator.sort(merged); - return new AuthorizationAdvisorProxyFactory(merged); + public AuthorizationAdvisorProxyFactory() { + List advisors = new ArrayList<>(); + advisors.add(AuthorizationManagerBeforeMethodInterceptor.preAuthorize()); + advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize()); + advisors.add(new PreFilterAuthorizationMethodInterceptor()); + advisors.add(new PostFilterAuthorizationMethodInterceptor()); + setAdvisors(advisors); } /** @@ -165,6 +153,30 @@ public Object proxy(Object target) { return factory.getProxy(); } + /** + * Add advisors that should be included to each proxy created. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + */ + public void setAdvisors(AuthorizationAdvisor... advisors) { + this.advisors = new ArrayList<>(List.of(advisors)); + AnnotationAwareOrderComparator.sort(this.advisors); + } + + /** + * Add advisors that should be included to each proxy created. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + */ + public void setAdvisors(Collection advisors) { + this.advisors = new ArrayList<>(advisors); + AnnotationAwareOrderComparator.sort(this.advisors); + } + @SuppressWarnings("unchecked") private T proxyCast(T target) { return (T) proxy(target); diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java index 2a7852b28cb..9ca1ce9b4df 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java @@ -41,7 +41,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.method.AuthorizationAdvisor; -import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -65,9 +64,7 @@ public class AuthorizationAdvisorProxyFactoryTests { @Test public void proxyWhenPreAuthorizeThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Flight flight = new Flight(); assertThat(flight.getAltitude()).isEqualTo(35000d); Flight secured = proxy(factory, flight); @@ -78,9 +75,7 @@ public void proxyWhenPreAuthorizeThenHonors() { @Test public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); assertThat(this.alan.getFirstName()).isEqualTo("alan"); User secured = proxy(factory, this.alan); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getFirstName); @@ -94,9 +89,7 @@ public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { @Test public void proxyWhenPreAuthorizeOnRecordThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); HasSecret repo = new Repository("secret"); assertThat(repo.secret()).isEqualTo("secret"); HasSecret secured = proxy(factory, repo); @@ -109,9 +102,7 @@ public void proxyWhenPreAuthorizeOnRecordThenHonors() { @Test public void proxyWhenImmutableListThenReturnsSecuredImmutableList() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); List flights = List.of(this.flight); List secured = proxy(factory, flights); secured.forEach( @@ -123,9 +114,7 @@ public void proxyWhenImmutableListThenReturnsSecuredImmutableList() { @Test public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Set flights = Set.of(this.flight); Set secured = proxy(factory, flights); secured.forEach( @@ -137,9 +126,7 @@ public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() { @Test public void proxyWhenQueueThenReturnsSecuredQueue() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Queue flights = new LinkedList<>(List.of(this.flight)); Queue secured = proxy(factory, flights); assertThat(flights.size()).isEqualTo(secured.size()); @@ -151,9 +138,7 @@ public void proxyWhenQueueThenReturnsSecuredQueue() { @Test public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedSet users = Collections.unmodifiableSortedSet(new TreeSet<>(Set.of(this.alan))); SortedSet secured = proxy(factory, users); secured @@ -165,9 +150,7 @@ public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() { @Test public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedMap users = Collections .unmodifiableSortedMap(new TreeMap<>(Map.of(this.alan.getId(), this.alan))); SortedMap secured = proxy(factory, users); @@ -180,9 +163,7 @@ public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() { @Test public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Map users = Map.of(this.alan.getId(), this.alan); Map secured = proxy(factory, users); secured.forEach( @@ -194,9 +175,7 @@ public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() { @Test public void proxyWhenMutableListThenReturnsSecuredMutableList() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); List flights = new ArrayList<>(List.of(this.flight)); List secured = proxy(factory, flights); secured.forEach( @@ -208,9 +187,7 @@ public void proxyWhenMutableListThenReturnsSecuredMutableList() { @Test public void proxyWhenMutableSetThenReturnsSecuredMutableSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Set flights = new HashSet<>(Set.of(this.flight)); Set secured = proxy(factory, flights); secured.forEach( @@ -222,9 +199,7 @@ public void proxyWhenMutableSetThenReturnsSecuredMutableSet() { @Test public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedSet users = new TreeSet<>(Set.of(this.alan)); SortedSet secured = proxy(factory, users); secured.forEach((u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -235,9 +210,7 @@ public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() { @Test public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedMap users = new TreeMap<>(Map.of(this.alan.getId(), this.alan)); SortedMap secured = proxy(factory, users); secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -248,9 +221,7 @@ public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() { @Test public void proxyWhenMutableMapThenReturnsSecuredMutableMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Map users = new HashMap<>(Map.of(this.alan.getId(), this.alan)); Map secured = proxy(factory, users); secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -261,9 +232,7 @@ public void proxyWhenMutableMapThenReturnsSecuredMutableMap() { @Test public void proxyWhenPreAuthorizeForOptionalThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Optional flights = Optional.of(this.flight); assertThat(flights.get().getAltitude()).isEqualTo(35000d); Optional secured = proxy(factory, flights); @@ -274,9 +243,7 @@ public void proxyWhenPreAuthorizeForOptionalThenHonors() { @Test public void proxyWhenPreAuthorizeForStreamThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Stream flights = Stream.of(this.flight); Stream secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(Flight::getAltitude)); @@ -286,9 +253,7 @@ public void proxyWhenPreAuthorizeForStreamThenHonors() { @Test public void proxyWhenPreAuthorizeForArrayThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Flight[] flights = { this.flight }; Flight[] secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured[0]::getAltitude); @@ -298,9 +263,7 @@ public void proxyWhenPreAuthorizeForArrayThenHonors() { @Test public void proxyWhenPreAuthorizeForIteratorThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Iterator flights = List.of(this.flight).iterator(); Iterator secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.next().getAltitude()); @@ -310,9 +273,7 @@ public void proxyWhenPreAuthorizeForIteratorThenHonors() { @Test public void proxyWhenPreAuthorizeForIterableThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Iterable users = new UserRepository(); Iterable secured = proxy(factory, users); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(User::getFirstName)); @@ -321,9 +282,7 @@ public void proxyWhenPreAuthorizeForIterableThenHonors() { @Test public void proxyWhenPreAuthorizeForClassThenHonors() { - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Class clazz = proxy(factory, Flight.class); assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); Flight secured = proxy(factory, this.flight); @@ -334,12 +293,12 @@ public void proxyWhenPreAuthorizeForClassThenHonors() { } @Test - public void withAdvisorsWhenProxyThenVisits() { + public void setAdvisorsWhenProxyThenVisits() { AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class); given(advisor.getAdvice()).willReturn(advisor); given(advisor.getPointcut()).willReturn(Pointcut.TRUE); AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); - factory = factory.withAdvisors(advisor); + factory.setAdvisors(advisor); Flight flight = proxy(factory, this.flight); flight.getAltitude(); verify(advisor, atLeastOnce()).getPointcut(); From c611b7e33b9ffcf40141b4bce0ac8a1f557434ff Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 14 Mar 2024 10:44:06 -0600 Subject: [PATCH 29/31] Add AuthorizationProxyFactory Reactive Support Issue gh-14596 --- ...ionManagerMethodSecurityConfiguration.java | 89 +++++-- ...activeAuthorizationProxyConfiguration.java | 45 ++++ .../ReactiveMethodSecuritySelector.java | 4 +- .../AuthorizationProxyConfigurationTests.java | 45 ++++ ...ctiveAuthorizationAdvisorProxyFactory.java | 137 +++++++++++ ...ManagerAfterReactiveMethodInterceptor.java | 6 +- ...anagerBeforeReactiveMethodInterceptor.java | 6 +- ...uthorizationReactiveMethodInterceptor.java | 6 +- ...uthorizationReactiveMethodInterceptor.java | 6 +- ...AuthorizationAdvisorProxyFactoryTests.java | 226 ++++++++++++++++++ 10 files changed, 534 insertions(+), 36 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java create mode 100644 core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java create mode 100644 core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 848934bb257..2bfa745f676 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -16,9 +16,18 @@ package org.springframework.security.config.annotation.method.configuration; +import java.util.function.Consumer; +import java.util.function.Supplier; + import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,6 +38,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -38,6 +48,7 @@ import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.util.function.SingletonSupplier; /** * Configuration for a {@link ReactiveAuthenticationManager} based Method Security. @@ -46,54 +57,56 @@ * @since 5.8 */ @Configuration(proxyBeanMethods = false) -final class ReactiveAuthorizationManagerMethodSecurityConfiguration { +final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor( - MethodSecurityExpressionHandler expressionHandler, + static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider) { PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor( expressionHandler); - defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults); - return interceptor; + return new DeferringMethodInterceptor<>(interceptor, + (i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults)); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor( + static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider, ObjectProvider registryProvider) { PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager( expressionHandler); - defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults); ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); - return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager); + AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor + .preAuthorize(authorizationManager); + return new DeferringMethodInterceptor<>(interceptor, + (i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults)); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor( - MethodSecurityExpressionHandler expressionHandler, + static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider) { PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor( expressionHandler); - defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults); - return interceptor; + return new DeferringMethodInterceptor<>(interceptor, + (i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults)); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor( + static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider, ObjectProvider registryProvider) { PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager( expressionHandler); ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); - defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults); - return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager); + AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor + .postAuthorize(authorizationManager); + return new DeferringMethodInterceptor<>(interceptor, + (i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults)); } @Bean @@ -112,4 +125,50 @@ static ReactiveAuthorizationManager manager(ReactiveAuthorizationManager< return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate); } + private static final class DeferringMethodInterceptor + implements AuthorizationAdvisor { + + private final Pointcut pointcut; + + private final int order; + + private final Supplier delegate; + + DeferringMethodInterceptor(M delegate, Consumer supplier) { + this.pointcut = delegate.getPointcut(); + this.order = delegate.getOrder(); + this.delegate = SingletonSupplier.of(() -> { + supplier.accept(delegate); + return delegate; + }); + } + + @Nullable + @Override + public Object invoke(@NotNull MethodInvocation invocation) throws Throwable { + return this.delegate.get().invoke(invocation); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public boolean isPerInstance() { + return true; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java new file mode 100644 index 00000000000..b7e3b07fbdc --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisor; + +@Configuration(proxyBeanMethods = false) +final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory( + ObjectProvider provider) { + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + factory.setAdvisors(advisors); + return factory; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index dd984bc1a8f..b1c923383e5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java @@ -51,13 +51,15 @@ public String[] selectImports(AnnotationMetadata importMetadata) { else { imports.add(ReactiveMethodSecurityConfiguration.class.getName()); } + imports.add(ReactiveAuthorizationProxyConfiguration.class.getName()); return imports.toArray(new String[0]); } private static final class AutoProxyRegistrarSelector extends AdviceModeImportSelector { - private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() }; + private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(), + MethodSecurityAdvisorRegistrar.class.getName() }; @Override protected String[] selectImports(@NonNull AdviceMode adviceMode) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java index e3e41c03167..254c8b08713 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java @@ -18,15 +18,20 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.AuthorizationProxyFactory; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -69,12 +74,42 @@ public void proxyWhenPreAuthorizedThenAllows() { assertThat(toaster.extractBread()).isEqualTo("yummy"); } + @Test + public void proxyReactiveWhenNotPreAuthorizedThenDenies() { + this.spring.register(ReactiveDefaultsConfig.class).autowire(); + Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster()); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .verifyError(AccessDeniedException.class); + StepVerifier + .create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .verifyError(AccessDeniedException.class); + } + + @Test + public void proxyReactiveWhenPreAuthorizedThenAllows() { + this.spring.register(ReactiveDefaultsConfig.class).autowire(); + Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster()); + Authentication admin = TestAuthentication.authenticatedAdmin(); + StepVerifier + .create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin))) + .expectNext() + .verifyComplete(); + } + @EnableMethodSecurity @Configuration static class DefaultsConfig { } + @EnableReactiveMethodSecurity + @Configuration + static class ReactiveDefaultsConfig { + + } + static class Toaster { @PreAuthorize("hasRole('ADMIN')") @@ -87,6 +122,16 @@ String extractBread() { return "yummy"; } + @PreAuthorize("hasRole('ADMIN')") + Mono reactiveMakeToast() { + return Mono.empty(); + } + + @PostAuthorize("hasRole('ADMIN')") + Mono reactiveExtractBread() { + return Mono.just("yummy"); + } + } } diff --git a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java new file mode 100644 index 00000000000..5720082862d --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor; +import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; + +/** + * A proxy factory for applying authorization advice to an arbitrary object. + * + *

+ * For example, consider a non-Spring-managed object {@code Foo}:

+ *     class Foo {
+ *         @PreAuthorize("hasAuthority('bar:read')")
+ *         String bar() { ... }
+ *     }
+ * 
+ * + * Use {@link ReactiveAuthorizationAdvisorProxyFactory} to wrap the instance in Spring + * Security's {@link org.springframework.security.access.prepost.PreAuthorize} method + * interceptor like so: + * + *
+ *     AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ *     AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
+ *     Foo foo = new Foo();
+ *     foo.bar(); // passes
+ *     Foo securedFoo = proxyFactory.proxy(foo);
+ *     securedFoo.bar(); // access denied!
+ * 
+ * + * @author Josh Cummings + * @since 6.3 + */ +public final class ReactiveAuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory { + + private final AuthorizationAdvisorProxyFactory defaults = new AuthorizationAdvisorProxyFactory(); + + public ReactiveAuthorizationAdvisorProxyFactory() { + List advisors = new ArrayList<>(); + advisors.add(AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize()); + advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize()); + advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor()); + advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor()); + this.defaults.setAdvisors(advisors); + } + + /** + * Proxy an object to enforce authorization advice. + * + *

+ * Proxies any instance of a non-final class or a class that implements more than one + * interface. + * + *

+ * If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array}, + * {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is + * proxied. + * + *

+ * If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is + * invoked instead. + * @param target the instance to proxy + * @return the proxied instance + */ + @Override + public Object proxy(Object target) { + if (target instanceof Mono mono) { + return proxyMono(mono); + } + if (target instanceof Flux flux) { + return proxyFlux(flux); + } + return this.defaults.proxy(target); + } + + /** + * Add advisors that should be included to each proxy created. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + */ + public void setAdvisors(AuthorizationAdvisor... advisors) { + this.defaults.setAdvisors(advisors); + } + + /** + * Add advisors that should be included to each proxy created. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + */ + public void setAdvisors(Collection advisors) { + this.defaults.setAdvisors(advisors); + } + + private Mono proxyMono(Mono mono) { + return mono.map(this::proxy); + } + + private Flux proxyFlux(Flux flux) { + return flux.map(this::proxy); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java index 550b8fbdef3..ea3d0236d8c 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java @@ -28,11 +28,8 @@ import reactor.core.publisher.Mono; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PostAuthorize; @@ -48,8 +45,7 @@ * @author Evgeniy Cheban * @since 5.8 */ -public final class AuthorizationManagerAfterReactiveMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class AuthorizationManagerAfterReactiveMethodInterceptor implements AuthorizationAdvisor { private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java index f3d1cce8bfa..25e43e15883 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java @@ -27,11 +27,8 @@ import reactor.core.publisher.Mono; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PreAuthorize; @@ -48,8 +45,7 @@ * @author Josh Cummings * @since 5.8 */ -public final class AuthorizationManagerBeforeReactiveMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class AuthorizationManagerBeforeReactiveMethodInterceptor implements AuthorizationAdvisor { private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java index 19dbbb7e0be..072bb5b75d5 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java @@ -26,9 +26,6 @@ import reactor.core.publisher.Mono; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.core.Ordered; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.expression.EvaluationContext; @@ -46,8 +43,7 @@ * @author Evgeniy Cheban * @since 5.8 */ -public final class PostFilterAuthorizationReactiveMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class PostFilterAuthorizationReactiveMethodInterceptor implements AuthorizationAdvisor { private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry(); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java index 18f7d61123b..19775eb126e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java @@ -26,10 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.aop.Pointcut; -import org.springframework.aop.PointcutAdvisor; -import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.support.AopUtils; -import org.springframework.core.Ordered; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; @@ -50,8 +47,7 @@ * @author Evgeniy Cheban * @since 5.8 */ -public final class PreFilterAuthorizationReactiveMethodInterceptor - implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { +public final class PreFilterAuthorizationReactiveMethodInterceptor implements AuthorizationAdvisor { private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry(); diff --git a/core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java new file mode 100644 index 00000000000..1dc8afccdce --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.Iterator; +import java.util.List; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.aop.Pointcut; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ReactiveAuthorizationAdvisorProxyFactoryTests { + + private final Authentication user = TestAuthentication.authenticatedUser(); + + private final Authentication admin = TestAuthentication.authenticatedAdmin(); + + private final Flight flight = new Flight(); + + private final User alan = new User("alan", "alan", "turing"); + + @Test + public void proxyWhenPreAuthorizeThenHonors() { + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + Flight flight = new Flight(); + StepVerifier + .create(flight.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .expectNext(35000d) + .verifyComplete(); + Flight secured = proxy(factory, flight); + StepVerifier + .create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .verifyError(AccessDeniedException.class); + } + + @Test + public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { + SecurityContextHolder.getContext().setAuthentication(this.user); + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + StepVerifier + .create(this.alan.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .expectNext("alan") + .verifyComplete(); + User secured = proxy(factory, this.alan); + StepVerifier + .create(secured.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .verifyError(AccessDeniedException.class); + StepVerifier + .create(secured.getFirstName() + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authenticated("alan")))) + .expectNext("alan") + .verifyComplete(); + StepVerifier + .create(secured.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.admin))) + .expectNext("alan") + .verifyComplete(); + } + + @Test + public void proxyWhenPreAuthorizeOnRecordThenHonors() { + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + HasSecret repo = new Repository(Mono.just("secret")); + StepVerifier.create(repo.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .expectNext("secret") + .verifyComplete(); + HasSecret secured = proxy(factory, repo); + StepVerifier.create(secured.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .verifyError(AccessDeniedException.class); + StepVerifier.create(secured.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.admin))) + .expectNext("secret") + .verifyComplete(); + } + + @Test + public void proxyWhenPreAuthorizeOnFluxThenHonors() { + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + Flux flights = Flux.just(this.flight); + Flux secured = proxy(factory, flights); + StepVerifier + .create(secured.flatMap(Flight::getAltitude) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .verifyError(AccessDeniedException.class); + } + + @Test + public void proxyWhenPreAuthorizeForClassThenHonors() { + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + Class clazz = proxy(factory, Flight.class); + assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); + Flight secured = proxy(factory, this.flight); + StepVerifier + .create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) + .verifyError(AccessDeniedException.class); + } + + @Test + public void setAdvisorsWhenProxyThenVisits() { + AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class); + given(advisor.getAdvice()).willReturn(advisor); + given(advisor.getPointcut()).willReturn(Pointcut.TRUE); + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + factory.setAdvisors(advisor); + Flight flight = proxy(factory, this.flight); + flight.getAltitude(); + verify(advisor, atLeastOnce()).getPointcut(); + } + + private Authentication authenticated(String user, String... authorities) { + return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build()); + } + + private T proxy(AuthorizationProxyFactory factory, Object target) { + return (T) factory.proxy(target); + } + + static class Flight { + + @PreAuthorize("hasRole('PILOT')") + Mono getAltitude() { + return Mono.just(35000d); + } + + } + + interface Identifiable { + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + Mono getFirstName(); + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + Mono getLastName(); + + } + + public static class User implements Identifiable, Comparable { + + private final String id; + + private final String firstName; + + private final String lastName; + + User(String id, String firstName, String lastName) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getId() { + return this.id; + } + + @Override + public Mono getFirstName() { + return Mono.just(this.firstName); + } + + @Override + public Mono getLastName() { + return Mono.just(this.lastName); + } + + @Override + public int compareTo(@NotNull User that) { + return this.id.compareTo(that.getId()); + } + + } + + static class UserRepository implements Iterable { + + List users = List.of(new User("1", "first", "last")); + + Flux findAll() { + return Flux.fromIterable(this.users); + } + + @NotNull + @Override + public Iterator iterator() { + return this.users.iterator(); + } + + } + + interface HasSecret { + + Mono secret(); + + } + + record Repository(@PreAuthorize("hasRole('ADMIN')") Mono secret) implements HasSecret { + } + +} From 9728f28380a18c7f7333a1472656ffefb7af0973 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:32:26 -0500 Subject: [PATCH 30/31] Add additional constants to OAuth2ParameterNames These additional constants are used for optional parameters in the Access Token Request for the OAuth 2.0 Token Exchange Grant. Issue gh-5199 --- .../oauth2/core/endpoint/OAuth2ParameterNames.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index d482d7c1968..125d2428730 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -182,6 +182,18 @@ public final class OAuth2ParameterNames { */ public static final String INTERVAL = "interval"; + /** + * {@code audience} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String AUDIENCE = "audience"; + + /** + * {@code resource} - used in Token Exchange Access Token Request. + * @since 6.3 + */ + public static final String RESOURCE = "resource"; + /** * {@code requested_token_type} - used in Token Exchange Access Token Request. * @since 6.3 From 0078462e410eefbe27b3550f88fba058231f45bb Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:39:21 -0500 Subject: [PATCH 31/31] Make constant public Issue gh-5199 --- .../security/oauth2/core/endpoint/OAuth2ParameterNames.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index 125d2428730..49a3fa933f3 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -204,7 +204,7 @@ public final class OAuth2ParameterNames { * {@code issued_token_type} - used in Token Exchange Access Token Response. * @since 6.3 */ - private static final String ISSUED_TOKEN_TYPE = "issued_token_type"; + public static final String ISSUED_TOKEN_TYPE = "issued_token_type"; /** * {@code subject_token} - used in Token Exchange Access Token Request.