diff --git a/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java new file mode 100644 index 00000000000..69b5ca357f5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ObjectPostProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 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; + +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Allows initialization of Objects. Typically this is used to call the {@link Aware} + * methods, {@link InitializingBean#afterPropertiesSet()}, and ensure that + * {@link DisposableBean#destroy()} has been invoked. + * + * @param the bound of the types of Objects this {@link ObjectPostProcessor} supports. + * @author Rob Winch + * @since 3.2 + */ +public interface ObjectPostProcessor { + + static ObjectPostProcessor identity() { + return new ObjectPostProcessor<>() { + @Override + public O postProcess(O object) { + return object; + } + }; + } + + /** + * Initialize the object possibly returning a modified instance that should be used + * instead. + * @param object the object to initialize + * @return the initialized version of the object + */ + O postProcess(O object); + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index 42939264605..2a45b5eedb6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; diff --git a/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java index 53a43d1ce98..9d63541438e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/annotation/ObjectPostProcessor.java @@ -28,8 +28,11 @@ * @param the bound of the types of Objects this {@link ObjectPostProcessor} supports. * @author Rob Winch * @since 3.2 + * @deprecated please use {@link org.springframework.security.config.ObjectPostProcessor} + * instead */ -public interface ObjectPostProcessor { +@Deprecated +public interface ObjectPostProcessor extends org.springframework.security.config.ObjectPostProcessor { /** * Initialize the object possibly returning a modified instance that should be used diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java index 7703c974bd5..bc02c6e3637 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java @@ -21,6 +21,7 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.util.Assert; /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 73e8a31a449..8c7888dc535 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -26,8 +26,8 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index e85fdb0886a..1ebd7b8f7a2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.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. @@ -28,7 +28,6 @@ import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -40,7 +39,7 @@ import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; @@ -57,6 +56,7 @@ * Exports the authentication {@link Configuration} * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 * */ @@ -197,15 +197,6 @@ private AuthenticationManager getAuthenticationManagerBean() { return lazyBean(AuthenticationManager.class); } - private static T getBeanOrNull(ApplicationContext applicationContext, Class type) { - try { - return applicationContext.getBean(type); - } - catch (NoSuchBeanDefinitionException notFound) { - return null; - } - } - private static class EnableGlobalAuthenticationAutowiredConfigurer extends GlobalAuthenticationConfigurerAdapter { private final ApplicationContext context; @@ -330,12 +321,9 @@ private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; } - PasswordEncoder passwordEncoder = getBeanOrNull(this.applicationContext, PasswordEncoder.class); - if (passwordEncoder == null) { - passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } - this.passwordEncoder = passwordEncoder; - return passwordEncoder; + this.passwordEncoder = this.applicationContext.getBeanProvider(PasswordEncoder.class) + .getIfUnique(PasswordEncoderFactories::createDelegatingPasswordEncoder); + return this.passwordEncoder; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 35fb14c3f33..60c68725d95 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -39,6 +39,7 @@ * {@link PasswordEncoder} is defined will wire this up too. * * @author Rob Winch + * @author Ngoc Nhan * @since 4.1 */ @Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER) @@ -121,11 +122,7 @@ else if (userDetailsServices.size() > 1) { * component, null otherwise. */ private T getBeanOrNull(Class type) { - String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type); - if (beanNames.length != 1) { - return null; - } - return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type); + return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique(); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 45fd3208c94..79259083fb4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -22,7 +22,7 @@ import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 6acd120958b..b8595db8bd1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -17,7 +17,7 @@ package org.springframework.security.config.annotation.authentication.configurers.userdetails; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsPasswordService; diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java index ef729c5b4a4..d3a8770721d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java @@ -30,7 +30,7 @@ import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.core.NativeDetector; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.util.Assert; /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java index bb5147fb301..f969dcf4710 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index 95e657aaa28..ffbd6fc2940 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.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. @@ -27,7 +27,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; @@ -69,7 +68,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -84,6 +83,7 @@ * * @author Rob Winch * @author EddĂș MelĂ©ndez + * @author Ngoc Nhan * @since 3.2 * @see EnableGlobalMethodSecurity * @deprecated Use {@link PrePostMethodSecurityConfiguration}, @@ -168,19 +168,19 @@ public void afterSingletonsInstantiated() { catch (Exception ex) { throw new RuntimeException(ex); } - PermissionEvaluator permissionEvaluator = getSingleBeanOrNull(PermissionEvaluator.class); + PermissionEvaluator permissionEvaluator = getBeanOrNull(PermissionEvaluator.class); if (permissionEvaluator != null) { this.defaultMethodExpressionHandler.setPermissionEvaluator(permissionEvaluator); } - RoleHierarchy roleHierarchy = getSingleBeanOrNull(RoleHierarchy.class); + RoleHierarchy roleHierarchy = getBeanOrNull(RoleHierarchy.class); if (roleHierarchy != null) { this.defaultMethodExpressionHandler.setRoleHierarchy(roleHierarchy); } - AuthenticationTrustResolver trustResolver = getSingleBeanOrNull(AuthenticationTrustResolver.class); + AuthenticationTrustResolver trustResolver = getBeanOrNull(AuthenticationTrustResolver.class); if (trustResolver != null) { this.defaultMethodExpressionHandler.setTrustResolver(trustResolver); } - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); if (grantedAuthorityDefaults != null) { this.defaultMethodExpressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } @@ -188,13 +188,8 @@ public void afterSingletonsInstantiated() { this.defaultMethodExpressionHandler = this.objectPostProcessor.postProcess(this.defaultMethodExpressionHandler); } - private T getSingleBeanOrNull(Class type) { - try { - return this.context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - } - return null; + private T getBeanOrNull(Class type) { + return this.context.getBeanProvider(type).getIfUnique(); } private void initializeMethodSecurityInterceptor() throws Exception { @@ -262,7 +257,7 @@ protected AccessDecisionManager accessDecisionManager() { decisionVoters.add(new Jsr250Voter()); } RoleVoter roleVoter = new RoleVoter(); - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); if (grantedAuthorityDefaults != null) { roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } @@ -373,7 +368,7 @@ public MethodSecurityMetadataSource methodSecurityMetadataSource() { sources.add(new SecuredAnnotationSecurityMetadataSource()); } if (isJsr250Enabled) { - GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class); + GrantedAuthorityDefaults grantedAuthorityDefaults = getBeanOrNull(GrantedAuthorityDefaults.class); Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context .getBean(Jsr250MethodSecurityMetadataSource.class); if (grantedAuthorityDefaults != null) { 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 97f529efd47..2d6f9a25258 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 @@ -18,7 +18,6 @@ import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -36,9 +35,9 @@ import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.Jsr250AuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -58,8 +57,15 @@ final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrast private final Jsr250AuthorizationManager authorizationManager = new Jsr250AuthorizationManager(); - private AuthorizationManagerBeforeMethodInterceptor methodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .jsr250(this.authorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor methodInterceptor; + + Jsr250MethodSecurityConfiguration( + ObjectProvider>> postProcessors) { + ObjectPostProcessor> postProcessor = postProcessors + .getIfUnique(ObjectPostProcessor::identity); + AuthorizationManager manager = postProcessor.postProcess(this.authorizationManager); + this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.jsr250(manager); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @@ -95,16 +101,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.methodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - AuthorizationManager observed = new ObservationAuthorizationManager<>(registry, - this.authorizationManager); - this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(observed); - } - @Autowired(required = false) void setEventPublisher(AuthorizationEventPublisher eventPublisher) { this.methodInterceptor.setAuthorizationEventPublisher(eventPublisher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java new file mode 100644 index 00000000000..53bef440626 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * 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 io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +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.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class MethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + +} 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 2ceb262a140..47d5d23f761 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 @@ -41,6 +41,9 @@ final class MethodSecuritySelector implements ImportSelector { private static final boolean isDataPresent = ClassUtils .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); + private static final boolean isObservabilityPresent = ClassUtils + .isPresent("io.micrometer.observation.ObservationRegistry", null); + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override @@ -64,6 +67,9 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { if (isDataPresent) { imports.add(AuthorizationProxyDataConfiguration.class.getName()); } + if (isObservabilityPresent) { + imports.add(MethodObservationConfiguration.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 3426529d489..a1a47e3c54c 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 @@ -16,8 +16,8 @@ package org.springframework.security.config.annotation.method.configuration; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; import org.springframework.aop.framework.AopInfrastructureBean; @@ -38,14 +38,16 @@ import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar; import org.springframework.security.aot.hint.SecurityHintsRegistrar; import org.springframework.security.authorization.AuthorizationEventPublisher; -import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -78,21 +80,29 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, Applicati private final PreFilterAuthorizationMethodInterceptor preFilterMethodInterceptor = new PreFilterAuthorizationMethodInterceptor(); - private AuthorizationManagerBeforeMethodInterceptor preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(this.preAuthorizeAuthorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor preAuthorizeMethodInterceptor; - private AuthorizationManagerAfterMethodInterceptor postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(this.postAuthorizeAuthorizationManager); + private final AuthorizationManagerAfterMethodInterceptor postAuthorizeMethodInterceptor; private final PostFilterAuthorizationMethodInterceptor postFilterMethodInterceptor = new PostFilterAuthorizationMethodInterceptor(); private final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - { + PrePostMethodSecurityConfiguration( + ObjectProvider>> preAuthorizeProcessor, + ObjectProvider>> postAuthorizeProcessor) { this.preFilterMethodInterceptor.setExpressionHandler(this.expressionHandler); this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler); this.postFilterMethodInterceptor.setExpressionHandler(this.expressionHandler); + AuthorizationManager preAuthorize = preAuthorizeProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.preAuthorizeAuthorizationManager); + this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize(preAuthorize); + AuthorizationManager postAuthorize = postAuthorizeProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.postAuthorizeAuthorizationManager); + this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor.postAuthorize(postAuthorize); } @Override @@ -144,17 +154,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.postFilterMethodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(new ObservationAuthorizationManager<>(registry, this.preAuthorizeAuthorizationManager)); - this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterMethodInterceptor - .postAuthorize(new ObservationAuthorizationManager<>(registry, this.postAuthorizeAuthorizationManager)); - } - @Autowired(required = false) void setAuthorizationEventPublisher(AuthorizationEventPublisher publisher) { this.preAuthorizeMethodInterceptor.setAuthorizationEventPublisher(publisher); 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 995e57d5892..7373d7b8c4e 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,8 +16,8 @@ package org.springframework.security.config.annotation.method.configuration; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; import org.springframework.aop.framework.AopInfrastructureBean; @@ -34,14 +34,16 @@ import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PostAuthorizeReactiveAuthorizationManager; import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager; import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; @@ -77,22 +79,30 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration private PostFilterAuthorizationReactiveMethodInterceptor postFilterMethodInterceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); - private AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeMethodInterceptor; + private final AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeMethodInterceptor; - private AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeMethodInterceptor; + private final AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeMethodInterceptor; @Autowired(required = false) - ReactiveAuthorizationManagerMethodSecurityConfiguration(MethodSecurityExpressionHandler expressionHandler) { + ReactiveAuthorizationManagerMethodSecurityConfiguration(MethodSecurityExpressionHandler expressionHandler, + ObjectProvider>> preAuthorizePostProcessor, + ObjectProvider>> postAuthorizePostProcessor) { if (expressionHandler != null) { this.preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler); this.preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(expressionHandler); this.postFilterMethodInterceptor = new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler); this.postAuthorizeAuthorizationManager = new PostAuthorizeReactiveAuthorizationManager(expressionHandler); } + ReactiveAuthorizationManager preAuthorize = preAuthorizePostProcessor + .getIfUnique(ObjectPostProcessor::identity) + .postProcess(this.preAuthorizeAuthorizationManager); this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeReactiveMethodInterceptor - .preAuthorize(this.preAuthorizeAuthorizationManager); + .preAuthorize(preAuthorize); + ReactiveAuthorizationManager postAuthorize = postAuthorizePostProcessor + .getIfAvailable(ObjectPostProcessor::identity) + .postProcess(this.postAuthorizeAuthorizationManager); this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterReactiveMethodInterceptor - .postAuthorize(this.postAuthorizeAuthorizationManager); + .postAuthorize(postAuthorize); } @Override @@ -117,17 +127,6 @@ void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) this.postFilterMethodInterceptor.setTemplateDefaults(templateDefaults); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - this.preAuthorizeMethodInterceptor = AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize( - new ObservationReactiveAuthorizationManager<>(registry, this.preAuthorizeAuthorizationManager)); - this.postAuthorizeMethodInterceptor = AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize( - new ObservationReactiveAuthorizationManager<>(registry, this.postAuthorizeAuthorizationManager)); - } - @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor preFilterAuthorizationMethodInterceptor( diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java new file mode 100644 index 00000000000..3f73480cb7b --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodObservationConfiguration.java @@ -0,0 +1,71 @@ +/* + * 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 io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; + +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.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveMethodObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> methodResultAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java index 7d1d241f16a..41f356772f3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.ImportAware; import org.springframework.context.annotation.Role; import org.springframework.core.type.AnnotationMetadata; @@ -82,6 +83,7 @@ static PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(Abstract @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Fallback static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( ReactiveMethodSecurityConfiguration configuration) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); 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 dbedbeab605..c204b33aaae 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 @@ -38,6 +38,9 @@ class ReactiveMethodSecuritySelector implements ImportSelector { private static final boolean isDataPresent = ClassUtils .isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null); + private static final boolean isObservabilityPresent = ClassUtils + .isPresent("io.micrometer.observation.ObservationRegistry", null); + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override @@ -58,6 +61,9 @@ public String[] selectImports(AnnotationMetadata importMetadata) { if (isDataPresent) { imports.add(AuthorizationProxyDataConfiguration.class.getName()); } + if (isObservabilityPresent) { + imports.add(ReactiveMethodObservationConfiguration.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/SecuredMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java index 7247e8044db..3230996d6f5 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 @@ -18,7 +18,6 @@ import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -37,9 +36,9 @@ import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.SecuredAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.context.SecurityContextHolderStrategy; /** @@ -58,8 +57,15 @@ final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfras private final SecuredAuthorizationManager authorizationManager = new SecuredAuthorizationManager(); - private AuthorizationManagerBeforeMethodInterceptor methodInterceptor = AuthorizationManagerBeforeMethodInterceptor - .secured(this.authorizationManager); + private final AuthorizationManagerBeforeMethodInterceptor methodInterceptor; + + SecuredMethodSecurityConfiguration( + ObjectProvider>> postProcessors) { + ObjectPostProcessor> postProcessor = postProcessors + .getIfUnique(ObjectPostProcessor::identity); + AuthorizationManager manager = postProcessor.postProcess(this.authorizationManager); + this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(manager); + } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @@ -90,16 +96,6 @@ void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityCont this.methodInterceptor.setSecurityContextHolderStrategy(securityContextHolderStrategy); } - @Autowired(required = false) - void setObservationRegistry(ObservationRegistry registry) { - if (registry.isNoop()) { - return; - } - AuthorizationManager observed = new ObservationAuthorizationManager<>(registry, - this.authorizationManager); - this.methodInterceptor = AuthorizationManagerBeforeMethodInterceptor.secured(observed); - } - @Autowired(required = false) void setEventPublisher(AuthorizationEventPublisher eventPublisher) { this.methodInterceptor.setAuthorizationEventPublisher(eventPublisher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java index 29058c10e69..ab46f90bd02 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java @@ -35,7 +35,8 @@ @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Import({ RSocketSecurityConfiguration.class, SecuritySocketAcceptorInterceptorConfiguration.class }) +@Import({ RSocketSecurityConfiguration.class, SecuritySocketAcceptorInterceptorConfiguration.class, + ReactiveObservationImportSelector.class }) public @interface EnableRSocketSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java index 50b7ae5f975..c868b29ba33 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-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. @@ -108,6 +108,7 @@ * @author Luis Felipe Vega * @author Manuel Tejeda * @author Ebert Toribio + * @author Ngoc Nhan * @since 5.2 */ public class RSocketSecurity { @@ -238,15 +239,12 @@ private T getBeanOrNull(Class beanClass) { return getBeanOrNull(ResolvableType.forClass(beanClass)); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { if (this.context == null) { return null; } - String[] names = this.context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) this.context.getBean(names[0]); - } - return null; + return (T) this.context.getBeanProvider(type).getIfUnique(); } protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException { diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java index 9f7f5c9c5ba..da5a5fcb1d2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurityConfiguration.java @@ -16,16 +16,14 @@ package org.springframework.security.config.annotation.rsocket; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; -import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -46,7 +44,7 @@ class RSocketSecurityConfiguration { private PasswordEncoder passwordEncoder; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); @Autowired(required = false) void setAuthenticationManager(ReactiveAuthenticationManager authenticationManager) { @@ -64,8 +62,8 @@ void setPasswordEncoder(PasswordEncoder passwordEncoder) { } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setAuthenticationManagerPostProcessor(ObjectPostProcessor postProcessor) { + this.postProcessor = postProcessor; } @Bean(name = RSOCKET_SECURITY_BEAN_NAME) @@ -86,10 +84,7 @@ private ReactiveAuthenticationManager authenticationManager() { if (this.passwordEncoder != null) { manager.setPasswordEncoder(this.passwordEncoder); } - if (!this.observationRegistry.isNoop()) { - return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); - } - return manager; + return this.postProcessor.postProcess(manager); } return null; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java new file mode 100644 index 00000000000..18f6f03e8fb --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * 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.rsocket; + +import io.micrometer.observation.ObservationRegistry; + +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.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.server.ObservationWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; +import org.springframework.web.server.ServerWebExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public WebFilterChainDecorator postProcess(WebFilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationWebFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java new file mode 100644 index 00000000000..6e18bc4396c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/ReactiveObservationImportSelector.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2013 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.rsocket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebFluxSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ReactiveObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ReactiveObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ReactiveObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 0f779f2df32..7be2a4ae4c7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -34,10 +34,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -113,7 +115,9 @@ public C anyRequest() { */ protected final List createMvcMatchers(HttpMethod method, String... mvcPatterns) { Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest"); - ObjectPostProcessor opp = this.context.getBean(ObjectPostProcessor.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> postProcessors = this.context.getBeanProvider(type); + ObjectPostProcessor opp = postProcessors.getObject(); if (!this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 12253d65da8..fa9b239da1e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; -import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -30,15 +29,16 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.ObservationAuthenticationManager; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; @@ -139,6 +139,7 @@ * * @author Rob Winch * @author Joe Grandja + * @author Ngoc Nhan * @since 3.2 * @see EnableWebSecurity */ @@ -1112,9 +1113,10 @@ public HttpSecurity rememberMe(Customizer> re * * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests()} instead + * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests(Customizer)} + * instead */ - @Deprecated + @Deprecated(since = "6.1", forRemoval = true) public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { ApplicationContext context = getContext(); @@ -1227,9 +1229,10 @@ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrl * for the {@link ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry} * @return the {@link HttpSecurity} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests} instead + * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests(Customizer)} + * instead */ - @Deprecated + @Deprecated(since = "6.1", forRemoval = true) public HttpSecurity authorizeRequests( Customizer.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer) throws Exception { @@ -3276,13 +3279,10 @@ protected void beforeConfigure() throws Exception { setSharedObject(AuthenticationManager.class, this.authenticationManager); } else { - ObservationRegistry registry = getObservationRegistry(); + ObjectPostProcessor postProcessor = getAuthenticationManagerPostProcessor(); AuthenticationManager manager = getAuthenticationRegistry().build(); - if (!registry.isNoop() && manager != null) { - setSharedObject(AuthenticationManager.class, new ObservationAuthenticationManager(registry, manager)); - } - else { - setSharedObject(AuthenticationManager.class, manager); + if (manager != null) { + setSharedObject(AuthenticationManager.class, postProcessor.postProcess(manager)); } } } @@ -3683,7 +3683,9 @@ private List createAntMatchers(String... patterns) { } private List createMvcMatchers(String... mvcPatterns) { - ObjectPostProcessor opp = getContext().getBean(ObjectPostProcessor.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> postProcessors = getContext().getBeanProvider(type); + ObjectPostProcessor opp = postProcessors.getObject(); if (!getContext().containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type " + HandlerMappingIntrospector.class.getName() @@ -3718,13 +3720,12 @@ private getAuthenticationManagerPostProcessor() { ApplicationContext context = getContext(); - String[] names = context.getBeanNamesForType(ObservationRegistry.class); - if (names.length == 1) { - return (ObservationRegistry) context.getBean(names[0]); - } - return ObservationRegistry.NOOP; + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + AuthenticationManager.class); + ObjectProvider> manager = context.getBeanProvider(type); + return manager.getIfUnique(ObjectPostProcessor::identity); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 684c5f8015a..43329137fcf 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -28,14 +28,16 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; @@ -45,8 +47,8 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.FilterChainProxy.FilterChainDecorator; import org.springframework.security.web.FilterInvocation; -import org.springframework.security.web.ObservationFilterChainDecorator; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer; @@ -110,6 +112,9 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder filterChainDecoratorPostProcessor = ObjectPostProcessor + .identity(); + private HttpServletRequestTransformer privilegeEvaluatorRequestTransformer; private DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler(); @@ -403,6 +408,11 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } catch (NoSuchBeanDefinitionException ex) { } + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + FilterChainDecorator.class); + ObjectProvider> postProcessor = applicationContext + .getBeanProvider(type); + this.filterChainDecoratorPostProcessor = postProcessor.getIfUnique(ObjectPostProcessor::identity); Class requestTransformerClass = HttpServletRequestTransformer.class; this.privilegeEvaluatorRequestTransformer = applicationContext.getBeanProvider(requestTransformerClass) .getIfUnique(); @@ -413,11 +423,8 @@ public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } - FilterChainProxy.FilterChainDecorator getFilterChainDecorator() { - if (this.observationRegistry.isNoop()) { - return new FilterChainProxy.VirtualFilterChainDecorator(); - } - return new ObservationFilterChainDecorator(this.observationRegistry); + FilterChainDecorator getFilterChainDecorator() { + return this.filterChainDecoratorPostProcessor.postProcess(new FilterChainProxy.VirtualFilterChainDecorator()); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index d00242377e2..84706ebae46 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -82,7 +82,7 @@ @Target(ElementType.TYPE) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, - HttpSecurityConfiguration.class }) + HttpSecurityConfiguration.class, ObservationImportSelector.class }) @EnableGlobalAuthentication public @interface EnableWebSecurity { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 39a3633db4c..9859130ca11 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -30,7 +29,7 @@ import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; @@ -56,6 +55,7 @@ * * @author Eleftheria Stein * @author Jinwoo Bae + * @author Ngoc Nhan * @since 5.4 */ @Configuration(proxyBeanMethods = false) @@ -226,21 +226,9 @@ private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; } - PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); - if (passwordEncoder == null) { - passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } - this.passwordEncoder = passwordEncoder; - return passwordEncoder; - } - - private T getBeanOrNull(Class type) { - try { - return this.applicationContext.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + this.passwordEncoder = this.applicationContext.getBeanProvider(PasswordEncoder.class) + .getIfUnique(PasswordEncoderFactories::createDelegatingPasswordEncoder); + return this.passwordEncoder; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 24bd18f75a5..13c9a1b3c07 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.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. @@ -29,7 +29,6 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.ListableBeanFactory; 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.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -118,11 +117,19 @@ OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() { @Configuration(proxyBeanMethods = false) static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - private OAuth2AuthorizedClientManager authorizedClientManager; + private final OAuth2AuthorizedClientManager authorizedClientManager; - private SecurityContextHolderStrategy securityContextHolderStrategy; + private final ObjectProvider securityContextHolderStrategy; - private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + private final OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + + OAuth2ClientWebMvcSecurityConfiguration(ObjectProvider authorizedClientManager, + ObjectProvider securityContextHolderStrategy, + OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { + this.authorizedClientManager = authorizedClientManager.getIfUnique(); + this.securityContextHolderStrategy = securityContextHolderStrategy; + this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; + } @Override public void addArgumentResolvers(List argumentResolvers) { @@ -130,31 +137,11 @@ public void addArgumentResolvers(List argumentRes if (authorizedClientManager != null) { OAuth2AuthorizedClientArgumentResolver resolver = new OAuth2AuthorizedClientArgumentResolver( authorizedClientManager); - if (this.securityContextHolderStrategy != null) { - resolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); - } + this.securityContextHolderStrategy.ifAvailable(resolver::setSecurityContextHolderStrategy); argumentResolvers.add(resolver); } } - @Autowired(required = false) - void setAuthorizedClientManager(List authorizedClientManagers) { - if (authorizedClientManagers.size() == 1) { - this.authorizedClientManager = authorizedClientManagers.get(0); - } - } - - @Autowired(required = false) - void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) { - this.securityContextHolderStrategy = strategy; - } - - @Autowired - void setAuthorizedClientManagerRegistrar( - OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { - this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; - } - private OAuth2AuthorizedClientManager getAuthorizedClientManager() { if (this.authorizedClientManager != null) { return this.authorizedClientManager; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java new file mode 100644 index 00000000000..6ffee81a2ce --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * 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.configuration; + +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletRequest; + +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.authentication.AuthenticationManager; +import org.springframework.security.authentication.ObservationAuthenticationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.FilterChainProxy.FilterChainDecorator; +import org.springframework.security.web.ObservationFilterChainDecorator; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthenticationManager postProcess(AuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public FilterChainDecorator postProcess(FilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java new file mode 100644 index 00000000000..202d150f2f5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/ObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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.configuration; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 7274923af78..282b49fb924 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -38,7 +38,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java index db4271329be..dc7b1c92ceb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java @@ -17,7 +17,7 @@ package org.springframework.security.config.annotation.web.configurers; import org.springframework.context.ApplicationContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 82b2d329940..42d034b89d2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -20,10 +20,11 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; @@ -32,9 +33,8 @@ import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManagers; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -68,6 +68,9 @@ public final class AuthorizeHttpRequestsConfigurer> postProcessor = ObjectPostProcessor + .identity(); + /** * Creates an instance. * @param context the {@link ApplicationContext} to use @@ -87,6 +90,11 @@ public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class); this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); } + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + ResolvableType.forClassWithGenerics(AuthorizationManager.class, HttpServletRequest.class)); + ObjectProvider>> provider = context + .getBeanProvider(type); + provider.ifUnique((postProcessor) -> this.postProcessor = postProcessor); } /** @@ -123,17 +131,6 @@ AuthorizationManagerRequestMatcherRegistry addFirst(RequestMatcher matcher, return this.registry; } - private ObservationRegistry getObservationRegistry() { - ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - String[] names = context.getBeanNamesForType(ObservationRegistry.class); - if (names.length == 1) { - return context.getBean(ObservationRegistry.class); - } - else { - return ObservationRegistry.NOOP; - } - } - /** * Registry for mapping a {@link RequestMatcher} to an {@link AuthorizationManager}. * @@ -173,12 +170,8 @@ private AuthorizationManager createAuthorizationManager() { + ". Try completing it with something like requestUrls()..hasRole('USER')"); Assert.state(this.mappingCount > 0, "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); - ObservationRegistry registry = getObservationRegistry(); RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build()); - if (registry.isNoop()) { - return manager; - } - return new ObservationAuthorizationManager<>(registry, manager); + return AuthorizeHttpRequestsConfigurer.this.postProcessor.postProcess(manager); } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index 241ef194763..f16db3bd3a1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -25,7 +25,7 @@ import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java index ac7bdcaf544..b86f16cca1d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.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. @@ -29,7 +29,7 @@ import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; @@ -75,6 +75,7 @@ * @param the type of {@link HttpSecurityBuilder} that is being configured * @author Rob Winch * @author Yanming Zhou + * @author Ngoc Nhan * @since 3.2 * @see org.springframework.security.config.annotation.web.builders.HttpSecurity#authorizeRequests() * @deprecated Use {@link AuthorizeHttpRequestsConfigurer} instead @@ -106,10 +107,9 @@ public final class ExpressionUrlAuthorizationConfigurer getExpressionHandler(H http) } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { - String[] roleHiearchyBeanNames = context.getBeanNamesForType(RoleHierarchy.class); - if (roleHiearchyBeanNames.length == 1) { - defaultHandler.setRoleHierarchy(context.getBean(roleHiearchyBeanNames[0], RoleHierarchy.class)); - } - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - defaultHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } - String[] permissionEvaluatorBeanNames = context.getBeanNamesForType(PermissionEvaluator.class); - if (permissionEvaluatorBeanNames.length == 1) { - PermissionEvaluator permissionEvaluator = context.getBean(permissionEvaluatorBeanNames[0], - PermissionEvaluator.class); - defaultHandler.setPermissionEvaluator(permissionEvaluator); - } + context.getBeanProvider(RoleHierarchy.class).ifUnique(defaultHandler::setRoleHierarchy); + context.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> defaultHandler + .setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix())); + context.getBeanProvider(PermissionEvaluator.class).ifUnique(defaultHandler::setPermissionEvaluator); } this.expressionHandler = postProcess(defaultHandler); return this.expressionHandler; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java index 0125a22baed..2b36664fbf5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurer.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. @@ -18,7 +18,6 @@ import java.util.UUID; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.RememberMeAuthenticationProvider; @@ -78,6 +77,7 @@ * * @author Rob Winch * @author EddĂș MelĂ©ndez + * @author Ngoc Nhan * @since 3.2 */ public final class RememberMeConfigurer> @@ -444,20 +444,12 @@ private C getSharedOrBean(H http, Class type) { if (shared != null) { return shared; } - return getBeanOrNull(type); - } - private T getBeanOrNull(Class type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index e7eae3f2831..712c89073f8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.List; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -67,6 +66,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 * @see RequestCache */ @@ -134,12 +134,8 @@ private T getBeanOrNull(Class type) { if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + + return context.getBeanProvider(type).getIfUnique(); } @SuppressWarnings("unchecked") diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java index ba4769d996b..a1b64f1ea0b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.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. @@ -56,6 +56,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 */ public final class ServletApiConfigurer> @@ -92,12 +93,9 @@ public void configure(H http) { } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.securityContextRequestFilter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + context.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.securityContextRequestFilter + .setRolePrefix(grantedAuthorityDefaults.getRolePrefix())); this.securityContextRequestFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); } this.securityContextRequestFilter = postProcess(this.securityContextRequestFilter); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 504d68262ce..fc4a2a38804 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.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. @@ -25,7 +25,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.GenericApplicationListenerAdapter; @@ -100,6 +99,7 @@ * * @author Rob Winch * @author Onur Kagan Ozcan + * @author Ngoc Nhan * @since 3.2 * @see SessionManagementFilter * @see ConcurrentSessionFilter @@ -630,12 +630,8 @@ private T getBeanOrNull(Class type) { if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + + return context.getBeanProvider(type).getIfUnique(); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java index 7f8c8e9a41f..c1ac9a1c58b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java @@ -28,7 +28,7 @@ import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.vote.AuthenticatedVoter; import org.springframework.security.access.vote.RoleVoter; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index 7f89cdf184e..a3818e2a9ac 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.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. @@ -18,7 +18,6 @@ import jakarta.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; @@ -74,6 +73,7 @@ * * * @author Rob Winch + * @author Ngoc Nhan * @since 3.2 */ public final class X509Configurer> @@ -214,20 +214,11 @@ private C getSharedOrBean(H http, Class type) { if (shared != null) { return shared; } - return getBeanOrNull(type); - } - - private T getBeanOrNull(Class type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); if (context == null) { return null; } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index ba334bd383c..17c3c73ca49 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -86,6 +86,7 @@ * * @author Joe Grandja * @author Parikshit Dutta + * @author Ngoc Nhan * @since 5.1 * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2AuthorizationCodeGrantFilter @@ -320,13 +321,10 @@ private OAuth2AccessTokenResponseClient get @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } + if (context == null) { + return null; } - return null; + return (T) context.getBeanProvider(type).getIfUnique(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index a6b5f7c52bf..d191bb740be 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -149,6 +149,7 @@ * * @author Joe Grandja * @author Kazuki Shimizu + * @author Ngoc Nhan * @since 5.0 * @see HttpSecurity#oauth2Login() * @see OAuth2AuthorizationRequestRedirectFilter @@ -446,12 +447,10 @@ private JwtDecoderFactory getJwtDecoderFactoryBean() { if (names.length > 1) { throw new NoUniqueBeanDefinitionException(type, names); } - if (names.length == 1) { - return (JwtDecoderFactory) this.getBuilder() - .getSharedObject(ApplicationContext.class) - .getBean(names[0]); - } - return null; + return (JwtDecoderFactory) this.getBuilder() + .getSharedObject(ApplicationContext.class) + .getBeanProvider(type) + .getIfUnique(); } private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() { @@ -503,15 +502,13 @@ private OAuth2UserService getOAuth2UserService() return (bean != null) ? bean : new DefaultOAuth2UserService(); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } + if (context == null) { + return null; } - return null; + return (T) context.getBeanProvider(type).getIfUnique(); } private void initDefaultLoginFilter(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java index 7b6634f9333..7a65826ab95 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.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. @@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; /** * A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance @@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator< OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { this.audience = clientRegistration.getClientId(); - this.issuer = clientRegistration.getProviderDetails().getIssuerUri(); + String issuer = clientRegistration.getProviderDetails().getIssuerUri(); + Assert.hasText(issuer, "Provider issuer cannot be null"); + this.issuer = issuer; } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java index 9e3eefc0e7d..1095350dc5b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java @@ -65,6 +65,7 @@ * * * @author Josh Cummings + * @author Ngoc Nhan * @since 6.2 * @see HttpSecurity#oidcLogout() * @see OidcBackChannelLogoutFilter @@ -283,15 +284,13 @@ void configure(B http) { http.addFilterBefore(filter, CsrfFilter.class); } + @SuppressWarnings("unchecked") private T getBeanOrNull(Class clazz) { ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(clazz); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } + if (context == null) { + return null; } - return null; + return (T) context.getBeanProvider(clazz).getIfUnique(); } private static final class EitherLogoutHandler implements LogoutHandler { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java index a8ae662e604..dcdd8ec3a41 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java @@ -21,7 +21,6 @@ import jakarta.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; @@ -133,8 +132,8 @@ private SecurityContextRepository getSecurityContextRepository(H http) { } private void configureOttGenerateFilter(H http) { - GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http)); - generateFilter.setGeneratedOneTimeTokenHandler(getGeneratedOneTimeTokenHandler(http)); + GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http), + getGeneratedOneTimeTokenHandler(http)); generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.generateTokenUrl)); http.addFilter(postProcess(generateFilter)); http.addFilter(DefaultResourcesFilter.css()); @@ -321,12 +320,8 @@ private C getBeanOrNull(H http, Class clazz) { if (context == null) { return null; } - try { - return context.getBean(clazz); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + + return context.getBeanProvider(clazz).getIfUnique(); } private Map hiddenInputs(HttpServletRequest request) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 29bcc10e6b5..b07b034d143 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.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. @@ -24,7 +24,6 @@ import jakarta.servlet.http.HttpServletRequest; import org.opensaml.core.Version; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -501,12 +500,7 @@ private C getBeanOrNull(B http, Class clazz) { if (context == null) { return null; } - try { - return context.getBean(clazz); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } + return context.getBeanProvider(clazz).getIfUnique(); } private void setSharedObject(B http, Class clazz, C object) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index d3e5dd912b4..92c7cef819f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -107,6 +107,7 @@ * Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}. * * @author Josh Cummings + * @author Ngoc Nhan * @since 5.6 * @see Saml2LogoutConfigurer */ @@ -336,10 +337,7 @@ private C getBeanOrNull(Class clazz) { if (this.context == null) { return null; } - if (this.context.getBeanNamesForType(clazz).length == 0) { - return null; - } - return this.context.getBean(clazz); + return this.context.getBeanProvider(clazz).getIfAvailable(); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java index baf82dc6354..349e3a66066 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.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. @@ -174,10 +174,7 @@ private C getBeanOrNull(Class clazz) { if (this.context == null) { return null; } - if (this.context.getBeanNamesForType(clazz).length == 0) { - return null; - } - return this.context.getBean(clazz); + return this.context.getBeanProvider(clazz).getIfAvailable(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java index 21ad642a070..f5f7a2ddc2d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurity.java @@ -86,7 +86,7 @@ @Target(ElementType.TYPE) @Documented @Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class, - ReactiveOAuth2ClientImportSelector.class }) + ReactiveOAuth2ClientImportSelector.class, ReactiveObservationImportSelector.class }) public @interface EnableWebFluxSecurity { } 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 index 07808221f82..7432a9c565e 100644 --- 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 @@ -30,7 +30,6 @@ 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; @@ -93,9 +92,16 @@ ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar( @Configuration(proxyBeanMethods = false) static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { - private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager; - private ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + private final ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; + + OAuth2ClientWebFluxSecurityConfiguration( + ObjectProvider authorizedClientManager, + ReactiveOAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { + this.authorizedClientManager = authorizedClientManager.getIfUnique(); + this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; + } @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { @@ -105,19 +111,6 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } } - @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; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java new file mode 100644 index 00000000000..663b0ba9b00 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationConfiguration.java @@ -0,0 +1,88 @@ +/* + * 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 io.micrometer.observation.ObservationRegistry; + +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.authentication.ObservationReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; +import org.springframework.security.web.server.ObservationWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; +import org.springframework.web.server.ServerWebExchange; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class ReactiveObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthorizationManager postProcess(ReactiveAuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationReactiveAuthorizationManager<>(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor authenticationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public ReactiveAuthenticationManager postProcess(ReactiveAuthenticationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthentications(); + return active ? new ObservationReactiveAuthenticationManager(r, object) : object; + } + }; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor filterChainDecoratorPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public WebFilterChainDecorator postProcess(WebFilterChainDecorator object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveRequests(); + return active ? new ObservationWebFilterChainDecorator(r) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java new file mode 100644 index 00000000000..5b4bdaebf0c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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 io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebFluxSecurity} to conditionally import observation configuration + * when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class ReactiveObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = ReactiveObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { ReactiveObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 40bbac985d7..8bc535908aa 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.security.config.annotation.web.reactive; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; @@ -29,10 +27,10 @@ import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.security.authentication.ObservationReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; @@ -67,7 +65,7 @@ class ServerHttpSecurityConfiguration { private ReactiveCompromisedPasswordChecker compromisedPasswordChecker; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); @Autowired(required = false) private BeanFactory beanFactory; @@ -98,8 +96,8 @@ void setUserDetailsPasswordService(ReactiveUserDetailsPasswordService userDetail } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setAuthenticationManagerPostProcessor(ObjectPostProcessor postProcessor) { + this.postProcessor = postProcessor; } @Autowired(required = false) @@ -169,10 +167,7 @@ private ReactiveAuthenticationManager authenticationManager() { } manager.setUserDetailsPasswordService(this.userDetailsPasswordService); manager.setCompromisedPasswordChecker(this.compromisedPasswordChecker); - if (!this.observationRegistry.isNoop()) { - return new ObservationReactiveAuthenticationManager(this.observationRegistry, manager); - } - return manager; + return this.postProcessor.postProcess(manager); } return null; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index 44778d921f6..94242e48460 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java @@ -19,20 +19,20 @@ import java.util.Arrays; import java.util.List; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; -import org.springframework.security.web.server.ObservationWebFilterChainDecorator; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.security.web.server.WebFilterChainProxy.DefaultWebFilterChainDecorator; +import org.springframework.security.web.server.WebFilterChainProxy.WebFilterChainDecorator; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.result.view.AbstractView; @@ -57,7 +57,7 @@ class WebFluxSecurityConfiguration { private List securityWebFilterChains; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor postProcessor = ObjectPostProcessor.identity(); static { isOAuth2Present = ClassUtils.isPresent(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, @@ -73,17 +73,16 @@ void setSecurityWebFilterChains(List securityWebFilterCh } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setFilterChainPostProcessor(ObjectPostProcessor postProcessor) { + this.postProcessor = postProcessor; } @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME) @Order(WEB_FILTER_CHAIN_FILTER_ORDER) WebFilterChainProxy springSecurityWebFilterChainFilter() { WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains()); - if (!this.observationRegistry.isNoop()) { - proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry)); - } + WebFilterChainDecorator decorator = this.postProcessor.postProcess(new DefaultWebFilterChainDecorator()); + proxy.setFilterChainDecorator(decorator); return proxy; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java index 2d3d6c00607..cd2baed7335 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java @@ -35,7 +35,7 @@ import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.vote.AffirmativeBased; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java index 8b10c3bc23d..f67f14f3430 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.java @@ -52,7 +52,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import(WebSocketMessageBrokerSecurityConfiguration.class) +@Import({ WebSocketMessageBrokerSecurityConfiguration.class, WebSocketObservationImportSelector.class }) public @interface EnableWebSocketSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java index b9b0aec90e4..635341c8bc8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java @@ -20,8 +20,6 @@ import java.util.List; import java.util.Map; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -33,8 +31,8 @@ import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -79,7 +77,7 @@ final class WebSocketMessageBrokerSecurityConfiguration private AuthorizationManager> authorizationManager = ANY_MESSAGE_AUTHENTICATED; - private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + private ObjectPostProcessor>> postProcessor = ObjectPostProcessor.identity(); private ApplicationContext context; @@ -106,9 +104,7 @@ public void configureClientInboundChannel(ChannelRegistration registration) { } AuthorizationManager> manager = this.authorizationManager; - if (!this.observationRegistry.isNoop()) { - manager = new ObservationAuthorizationManager<>(this.observationRegistry, manager); - } + manager = this.postProcessor.postProcess(manager); AuthorizationChannelInterceptor interceptor = new AuthorizationChannelInterceptor(manager); interceptor.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); @@ -128,8 +124,9 @@ void setAuthorizationManager(AuthorizationManager> authorizationManag } @Autowired(required = false) - void setObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; + void setMessageAuthorizationManagerPostProcessor( + ObjectPostProcessor>> postProcessor) { + this.postProcessor = postProcessor; } @Autowired(required = false) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java new file mode 100644 index 00000000000..7d0fb806d59 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.socket; + +import io.micrometer.observation.ObservationRegistry; + +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.messaging.Message; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.observation.SecurityObservationSettings; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class WebSocketObservationConfiguration { + + private static final SecurityObservationSettings all = SecurityObservationSettings.withDefaults() + .shouldObserveRequests(true) + .shouldObserveAuthentications(true) + .shouldObserveAuthorizations(true) + .build(); + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ObjectPostProcessor>> webAuthorizationManagerPostProcessor( + ObjectProvider registry, ObjectProvider predicate) { + return new ObjectPostProcessor<>() { + @Override + public AuthorizationManager postProcess(AuthorizationManager object) { + ObservationRegistry r = registry.getIfUnique(() -> ObservationRegistry.NOOP); + boolean active = !r.isNoop() && predicate.getIfUnique(() -> all).shouldObserveAuthorizations(); + return active ? new ObservationAuthorizationManager<>(r, object) : object; + } + }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java new file mode 100644 index 00000000000..3eb4e0b4454 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketObservationImportSelector.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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.socket; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +/** + * Used by {@link EnableWebSocketSecurity} to conditionally import observation + * configuration when {@link ObservationRegistry} is present. + * + * @author Josh Cummings + * @since 6.4 + */ +class WebSocketObservationImportSelector implements ImportSelector { + + private static final boolean observabilityPresent; + + static { + ClassLoader classLoader = WebSocketObservationImportSelector.class.getClassLoader(); + observabilityPresent = ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", classLoader); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + if (!observabilityPresent) { + return new String[0]; + } + return new String[] { WebSocketObservationConfiguration.class.getName() }; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java index 9e2b6a8a65d..cbb7cfa0dcb 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -40,6 +40,7 @@ * has forgotten to declare the <authentication-manager> element. * * @author Luke Taylor + * @author Ngoc Nhan * @since 3.0 */ public class AuthenticationManagerFactoryBean implements FactoryBean, BeanFactoryAware { @@ -61,13 +62,13 @@ public AuthenticationManager getObject() throws Exception { if (!BeanIds.AUTHENTICATION_MANAGER.equals(ex.getBeanName())) { throw ex; } - UserDetailsService uds = getBeanOrNull(UserDetailsService.class); + UserDetailsService uds = this.bf.getBeanProvider(UserDetailsService.class).getIfUnique(); if (uds == null) { throw new NoSuchBeanDefinitionException(BeanIds.AUTHENTICATION_MANAGER, MISSING_BEAN_ERROR_MESSAGE); } DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(uds); - PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + PasswordEncoder passwordEncoder = this.bf.getBeanProvider(PasswordEncoder.class).getIfUnique(); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } @@ -99,13 +100,4 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; } - private T getBeanOrNull(Class type) { - try { - return this.bf.getBean(type); - } - catch (NoSuchBeanDefinitionException noUds) { - return null; - } - } - } diff --git a/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java b/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java index 611e46cbfad..570edfd764f 100644 --- a/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java +++ b/config/src/main/java/org/springframework/security/config/http/GrantedAuthorityDefaultsParserUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-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. @@ -25,6 +25,7 @@ /** * @author Rob Winch + * @author Ngoc Nhan * @since 4.2 */ final class GrantedAuthorityDefaultsParserUtils { @@ -49,13 +50,8 @@ abstract static class AbstractGrantedAuthorityDefaultsBeanFactory implements App @Override public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.rolePrefix = grantedAuthorityDefaults.getRolePrefix()); } abstract Object getBean(); diff --git a/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java index 70bb1965799..a2717d5be27 100644 --- a/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/GlobalMethodSecurityBeanDefinitionParser.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. @@ -88,6 +88,7 @@ * @author Ben Alex * @author Luke Taylor * @author Rob Winch + * @author Ngoc Nhan * @since 2.0 * @deprecated Use {@link MethodSecurityBeanDefinitionParser} instead */ @@ -483,13 +484,8 @@ abstract static class AbstractGrantedAuthorityDefaultsBeanFactory implements App @Override public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.rolePrefix = grantedAuthorityDefaults.getRolePrefix()); } } diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java index 8bde3921433..fef5a1a3549 100644 --- a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.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. @@ -67,6 +67,7 @@ * Processes the top-level "method-security" element. * * @author Josh Cummings + * @author Ngoc Nhan * @since 5.6 */ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser { @@ -307,13 +308,9 @@ public Class getObjectType() { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.expressionHandler + .setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix())); } } @@ -347,13 +344,9 @@ public Class getObjectType() { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - String[] grantedAuthorityDefaultsBeanNames = applicationContext - .getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length == 1) { - GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext - .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); - this.manager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); - } + applicationContext.getBeanProvider(GrantedAuthorityDefaults.class) + .ifUnique((grantedAuthorityDefaults) -> this.manager + .setRolePrefix(grantedAuthorityDefaults.getRolePrefix())); } public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { diff --git a/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java new file mode 100644 index 00000000000..d37d4218703 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/observation/SecurityObservationSettings.java @@ -0,0 +1,115 @@ +/* + * 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.observation; + +import io.micrometer.observation.ObservationPredicate; + +/** + * An {@link ObservationPredicate} that can be used to change which Spring Security + * observations are made with Micrometer. + * + *

+ * By default, web requests are not observed and authentications and authorizations are + * observed. + * + * @author Josh Cummings + * @since 6.4 + */ +public final class SecurityObservationSettings { + + private final boolean observeRequests; + + private final boolean observeAuthentications; + + private final boolean observeAuthorizations; + + private SecurityObservationSettings(boolean observeRequests, boolean observeAuthentications, + boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + /** + * Make no Spring Security observations + * @return a {@link SecurityObservationSettings} with all exclusions turned on + */ + public static SecurityObservationSettings noObservations() { + return new SecurityObservationSettings(false, false, false); + } + + /** + * Begin the configuration of a {@link SecurityObservationSettings} + * @return a {@link Builder} where filter chain observations are off and authn/authz + * observations are on + */ + public static Builder withDefaults() { + return new Builder(false, true, true); + } + + public boolean shouldObserveRequests() { + return this.observeRequests; + } + + public boolean shouldObserveAuthentications() { + return this.observeAuthentications; + } + + public boolean shouldObserveAuthorizations() { + return this.observeAuthorizations; + } + + /** + * A builder for configuring a {@link SecurityObservationSettings} + */ + public static final class Builder { + + private boolean observeRequests; + + private boolean observeAuthentications; + + private boolean observeAuthorizations; + + Builder(boolean observeRequests, boolean observeAuthentications, boolean observeAuthorizations) { + this.observeRequests = observeRequests; + this.observeAuthentications = observeAuthentications; + this.observeAuthorizations = observeAuthorizations; + } + + public Builder shouldObserveRequests(boolean excludeFilters) { + this.observeRequests = excludeFilters; + return this; + } + + public Builder shouldObserveAuthentications(boolean excludeAuthentications) { + this.observeAuthentications = excludeAuthentications; + return this; + } + + public Builder shouldObserveAuthorizations(boolean excludeAuthorizations) { + this.observeAuthorizations = excludeAuthorizations; + return this; + } + + public SecurityObservationSettings build() { + return new SecurityObservationSettings(this.observeRequests, this.observeAuthentications, + this.observeAuthorizations); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java index f36e48a2cb0..c522910a5c7 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.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. @@ -16,6 +16,10 @@ package org.springframework.security.config.web.server; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; +import com.nimbusds.jose.proc.JOSEObjectTypeVerifier; +import com.nimbusds.jose.proc.JWKSecurityContext; import reactor.core.publisher.Mono; import org.springframework.security.authentication.AuthenticationProvider; @@ -23,19 +27,22 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely @@ -61,9 +68,27 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti * Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager} */ OidcBackChannelLogoutReactiveAuthenticationManager() { - ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory(); - logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory()); - this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; + DefaultOidcLogoutTokenValidatorFactory jwtValidator = new DefaultOidcLogoutTokenValidatorFactory(); + this.logoutTokenDecoderFactory = (clientRegistration) -> { + String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri(); + if (!StringUtils.hasText(jwkSetUri)) { + OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier", + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + + "'. Check to ensure you have configured the JwkSet URI.", + null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + JOSEObjectTypeVerifier typeVerifier = new DefaultJOSEObjectTypeVerifier<>(null, + JOSEObjectType.JWT, new JOSEObjectType("logout+jwt")); + NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri) + .jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(typeVerifier)) + .build(); + decoder.setJwtValidator(jwtValidator.apply(clientRegistration)); + decoder.setClaimSetConverter( + new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters())); + return decoder; + }; } /** diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java index 7053689171e..c1709e1ec77 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.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. @@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; /** * A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance @@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator< OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { this.audience = clientRegistration.getClientId(); - this.issuer = clientRegistration.getProviderDetails().getIssuerUri(); + String issuer = clientRegistration.getProviderDetails().getIssuerUri(); + Assert.hasText(issuer, "Provider issuer cannot be null"); + this.issuer = issuer; } @Override diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index cd52e80738a..8c50ecee2b4 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; @@ -33,13 +34,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import reactor.util.context.Context; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -55,9 +56,9 @@ import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -1734,26 +1735,34 @@ private T getBean(Class beanClass) { } private T getBeanOrDefault(Class beanClass, T defaultInstance) { - T bean = getBeanOrNull(beanClass); - if (bean == null) { + if (this.context == null) { return defaultInstance; } - return bean; + return this.context.getBeanProvider(beanClass).getIfUnique(() -> defaultInstance); + } + + private ObjectProvider getBeanProvider(ResolvableType type) { + if (this.context == null) { + return new ObjectProvider<>() { + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + }; + } + return this.context.getBeanProvider(type); } private T getBeanOrNull(Class beanClass) { return getBeanOrNull(ResolvableType.forClass(beanClass)); } + @SuppressWarnings("unchecked") private T getBeanOrNull(ResolvableType type) { if (this.context == null) { return null; } - String[] names = this.context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) this.context.getBean(names[0]); - } - return null; + return (T) this.context.getBeanProvider(type).getIfUnique(); } private T getBeanOrNull(String beanName, Class requiredClass) { @@ -1799,6 +1808,17 @@ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegis private PathPatternParser pathPatternParser; + private ObjectPostProcessor> postProcessor = ObjectPostProcessor + .identity(); + + public AuthorizeExchangeSpec() { + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, + ResolvableType.forClassWithGenerics(ReactiveAuthorizationManager.class, ServerWebExchange.class)); + ObjectProvider>> postProcessor = getBeanProvider( + type); + postProcessor.ifUnique((p) -> this.postProcessor = p); + } + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -1851,10 +1871,7 @@ protected void configure(ServerHttpSecurity http) { Assert.state(this.matcher == null, () -> "The matcher " + this.matcher + " does not have an access rule defined"); ReactiveAuthorizationManager manager = this.managerBldr.build(); - ObservationRegistry registry = getBeanOrDefault(ObservationRegistry.class, ObservationRegistry.NOOP); - if (!registry.isNoop()) { - manager = new ObservationReactiveAuthorizationManager<>(registry, manager); - } + manager = this.postProcessor.postProcess(manager); AuthorizationWebFilter result = new AuthorizationWebFilter(manager); http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION); } @@ -4813,11 +4830,22 @@ public OAuth2ClientSpec authenticationManager(ReactiveAuthenticationManager auth private ReactiveAuthenticationManager getAuthenticationManager() { if (this.authenticationManager == null) { this.authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager( - new WebClientReactiveAuthorizationCodeTokenResponseClient()); + getAuthorizationCodeTokenResponseClient()); } return this.authenticationManager; } + private ReactiveOAuth2AccessTokenResponseClient getAuthorizationCodeTokenResponseClient() { + ResolvableType resolvableType = ResolvableType.forClassWithGenerics( + ReactiveOAuth2AccessTokenResponseClient.class, OAuth2AuthorizationCodeGrantRequest.class); + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOrNull( + resolvableType); + if (accessTokenResponseClient == null) { + accessTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient(); + } + return accessTokenResponseClient; + } + /** * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look * the value up as a Bean. diff --git a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java index f8c6c97f6d7..4fdd734dcd9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterClosureTests.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.security.config.ObjectPostProcessor; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java index 84411b85037..24f3be601f7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/SecurityConfigurerAdapterTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; +import org.springframework.security.config.ObjectPostProcessor; import static org.assertj.core.api.Assertions.assertThat; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java index f869ef5c498..dc2690e3bf4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.java @@ -34,7 +34,7 @@ import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 387ca313d47..a25e121073b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -42,8 +42,8 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AlreadyBuiltException; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java index 09ac66e347c..7730a0606b9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurerTests.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; diff --git a/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java b/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java index f02375507e0..75b5674248f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java @@ -37,7 +37,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.NativeDetector; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.web.context.ServletContextAware; diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index b856daf200a..73e65c78a7f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -28,6 +28,10 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.annotation.security.DenyAll; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -40,9 +44,12 @@ import org.springframework.aop.config.AopConfigUtils; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; +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.config.BeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Bean; @@ -80,6 +87,7 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener; @@ -107,6 +115,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link PrePostMethodSecurityConfiguration}. @@ -1018,6 +1027,80 @@ public void methodWhenMetaAnnotationPropertiesHasClassProperties() { assertThat(service.getIdPath("uid")).isEqualTo("uid"); } + @Test + @WithMockUser + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void securedMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void jsr250MethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class).autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(this.methodSecurityService::jsr250RolesAllowed); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.preAuthorizePermitAll(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void securedMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.securedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + + @Test + @WithMockUser + public void jsr250MethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceEnabledConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + this.methodSecurityService.jsr250RolesAllowedUser(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verifyNoInteractions(handler); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1655,4 +1738,57 @@ public Class getObjectType() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index cf845ebb612..042ed87c7e7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -23,14 +23,21 @@ import java.util.function.Consumer; import java.util.function.Function; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.beans.BeansException; +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.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; @@ -50,6 +57,7 @@ import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; import org.springframework.security.config.Customizer; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; @@ -62,7 +70,9 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Tadaya Tsuyukubo @@ -235,6 +245,46 @@ void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() { verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class)); } + @Test + public void prePostMethodWhenObservationRegistryThenObserved() { + this.spring.register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectNextCount(1) + .verifyComplete(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + verify(handler).onStart(any()); + verify(handler).onStop(any()); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verify(handler).onError(any()); + } + + @Test + @WithMockUser + public void prePostMethodWhenExcludeAuthorizationObservationsThenUnobserved() { + this.spring + .register(MethodSecurityServiceConfig.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + Authentication user = TestAuthentication.authenticatedUser(); + StepVerifier + .create(service.preAuthorizeUser().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectNextCount(1) + .verifyComplete(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + StepVerifier + .create(service.preAuthorizeAdmin().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user))) + .expectError() + .verify(); + verifyNoInteractions(handler); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -388,4 +438,58 @@ MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() { } + @Configuration + @EnableReactiveMethodSecurity + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index a3fee081025..05e94d8a30e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -48,6 +48,12 @@ @ReactiveMethodSecurityService.Mask("classmask") public interface ReactiveMethodSecurityService { + @PreAuthorize("hasRole('USER')") + Mono preAuthorizeUser(); + + @PreAuthorize("hasRole('ADMIN')") + Mono preAuthorizeAdmin(); + @PreAuthorize("hasRole('ADMIN')") @HandleAuthorizationDenied(handlerClass = StarMaskingHandler.class) Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java index 7b8a893d171..590184f67f6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java @@ -25,6 +25,16 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurityService { + @Override + public Mono preAuthorizeUser() { + return Mono.just("user"); + } + + @Override + public Mono preAuthorizeAdmin() { + return Mono.just("admin"); + } + @Override public Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber) { return Mono.just(cardNumber); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java index 9c2c1f0a146..322459e33ac 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index 411f92f9cba..8561390515e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -25,13 +25,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.config.MockServletContext; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestMockHttpServletMappings; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; @@ -79,7 +81,11 @@ public O postProcess(O object) { public void setUp() { this.matcherRegistry = new TestRequestMatcherRegistry(); this.context = mock(WebApplicationContext.class); - given(this.context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR); + ObjectProvider> postProcessors = mock(ObjectProvider.class); + ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); + ObjectProvider> given = this.context.getBeanProvider(type); + given(given).willReturn(postProcessors); + given(postProcessors.getObject()).willReturn(NO_OP_OBJECT_POST_PROCESSOR); given(this.context.getServletContext()).willReturn(MockServletContext.mvc()); this.matcherRegistry.setApplicationContext(this.context); mockMvcIntrospector(true); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 0aad4d777ca..168f89f1374 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -18,12 +18,20 @@ import java.util.function.Supplier; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; @@ -33,16 +41,19 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.authorization.AuthorizationObservationContext; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.observation.SecurityObservationSettings; 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.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -63,11 +74,14 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -153,7 +167,8 @@ public void configureMvcMatcherAccessAuthorizationManagerWhenNullThenException() @Test public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - ObjectPostProcessor objectPostProcessor = this.spring.getContext().getBean(ObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessorConfig.class).objectPostProcessor; verify(objectPostProcessor).postProcess(any(RequestMatcherDelegatingAuthorizationManager.class)); verify(objectPostProcessor).postProcess(any(AuthorizationFilter.class)); } @@ -623,6 +638,32 @@ public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Excep this.mvc.perform(requestWithUser).andExpect(status().isOk()); } + @Test + public void getWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class).autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthorizationObservationContext); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verify(handler).onError(any()); + } + + @Test + public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Exception { + this.spring + .register(RoleUserConfig.class, BasicController.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(user("user").roles("USER"))).andExpect(status().isOk()); + this.mvc.perform(get("/").with(user("user").roles("WRONG"))).andExpect(status().isForbidden()); + verifyNoInteractions(handler); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1015,6 +1056,12 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // @formatter:on } + @Bean + UserDetailsService users() { + return new InMemoryUserDetailsManager( + User.withUsername("user").password("{noop}password").roles("USER").build()); + } + } @Configuration @@ -1212,4 +1259,57 @@ void rootPost() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index 3697ef1b1cb..8ff4f513611 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index f65a1e1de85..7efdc87ee58 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java index 00b678c22e0..d89526127e2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.java @@ -27,7 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java index 4d163bbbd4a..ebb6b48bae8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurerTests.java @@ -39,7 +39,7 @@ import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.RememberMeAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.core.GrantedAuthorityDefaults; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 48313a867b3..49b8ed2a1af 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java index 31468c5711f..2a75aa5cc91 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index 25cfac15e6f..421289ddf4f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -16,20 +16,30 @@ package org.springframework.security.config.annotation.web.configurers; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationObservationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.AuthenticationException; @@ -50,8 +60,11 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.springframework.security.config.Customizer.withDefaults; @@ -161,6 +174,43 @@ public void httpBasicWhenUsingCustomSecurityContextRepositoryThenUses() throws E .saveContext(any(SecurityContext.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); } + @Test + public void httpBasicWhenObservationRegistryThenObserves() throws Exception { + this.spring.register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class).autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).anyMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler).onError(context.capture()); + assertThat(context.getValue()).isInstanceOf(AuthenticationObservationContext.class); + } + + @Test + public void httpBasicWhenExcludeAuthenticationObservationsThenUnobserved() throws Exception { + this.spring + .register(HttpBasic.class, Users.class, Home.class, ObservationRegistryConfig.class, + SelectableObservationsConfig.class) + .autowire(); + ObservationHandler handler = this.spring.getContext().getBean(ObservationHandler.class); + this.mvc.perform(get("/").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + ArgumentCaptor context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStart(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + context = ArgumentCaptor.forClass(Observation.Context.class); + verify(handler, atLeastOnce()).onStop(context.capture()); + assertThat(context.getAllValues()).noneMatch((c) -> c instanceof AuthenticationObservationContext); + this.mvc.perform(get("/").with(httpBasic("user", "wrong"))).andExpect(status().isUnauthorized()); + verify(handler, never()).onError(any()); + } + @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -384,4 +434,57 @@ String home(@AuthenticationPrincipal UserDetails user) { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthentications(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java index 57ffcff48a8..327c755a6aa 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java @@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index aff4d8a3758..ea62cd7046b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -29,7 +29,7 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -110,8 +110,8 @@ public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThe @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - ObjectPostProcessor objectPostProcessor = this.spring.getContext() - .getBean(ObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessorConfig.class).objectPostProcessor; verify(objectPostProcessor).postProcess(any(LogoutFilter.class)); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index c6bab8cf229..9c39f2b01c5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -33,7 +33,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -102,7 +102,7 @@ public void postWhenNoUserDetailsServiceThenException() { @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnRememberMeAuthenticationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - verify(this.spring.getContext().getBean(ObjectPostProcessor.class)) + verify(this.spring.getContext().getBean(ObjectPostProcessorConfig.class).objectPostProcessor) .postProcess(any(RememberMeAuthenticationFilter.class)); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index 1b14a87b335..f22e55043d9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -29,7 +29,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java index fee452f1282..5de55764f45 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java @@ -28,8 +28,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestDeferredSecurityContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.TestHttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java index 8a5993e25eb..9c1a47cf90c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java @@ -34,7 +34,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index 35e082e69fc..fbe52459a45 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -41,8 +41,8 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.config.Customizer; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.TestDeferredSecurityContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index f177dd4ffb6..206c0b7ecee 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java index 2b85cb72a26..653b9f6f67f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java @@ -93,6 +93,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willThrow; @@ -295,6 +296,22 @@ void logoutWhenCustomComponentsThenUses() throws Exception { verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class)); } + @Test + void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws Exception { + this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class) + .autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + MockHttpSession session = login(); + String logoutToken = this.mvc.perform(get("/token/logout").session(session)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + assertThatIllegalArgumentException().isThrownBy( + () -> this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .param("logout_token", logoutToken))); + } + private MockHttpSession login() throws Exception { MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher(); this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized()); @@ -523,6 +540,54 @@ LogoutHandler logoutHandler() { } + @Configuration + static class ProviderIssuerMissingRegistrationConfig { + + @Autowired(required = false) + MockWebServer web; + + @Bean + ClientRegistration clientRegistration() { + if (this.web == null) { + return TestClientRegistrations.clientRegistration().issuerUri(null).build(); + } + String issuer = this.web.url("/").toString(); + return TestClientRegistrations.clientRegistration() + .issuerUri(null) + .jwkSetUri(issuer + "jwks") + .tokenUri(issuer + "token") + .userInfoUri(issuer + "user") + .scope("openid") + .build(); + } + + @Bean + ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) { + return new InMemoryClientRegistrationRepository(clientRegistration); + } + + } + + @Configuration + @EnableWebSecurity + @Import(ProviderIssuerMissingRegistrationConfig.class) + static class ProviderIssuerMissingConfig { + + @Bean + @Order(1) + SecurityFilterChain filters(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); + // @formatter:on + + return http.build(); + } + + } + @Configuration @EnableWebSecurity @EnableWebMvc @@ -561,6 +626,9 @@ private static JWKSource jwks(RSAKey key) { @Autowired ClientRegistration registration; + @Autowired(required = false) + MockWebServer web; + @Bean @Order(0) SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception { @@ -597,7 +665,7 @@ Map accessToken(HttpServletRequest request) { HttpSession session = request.getSession(); JwtEncoderParameters parameters = JwtEncoderParameters .from(JwtClaimsSet.builder().id("id").subject(this.username) - .issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now()) + .issuer(getIssuerUri()).issuedAt(Instant.now()) .expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build()); String token = this.encoder.encode(parameters).getTokenValue(); return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null) @@ -605,7 +673,7 @@ Map accessToken(HttpServletRequest request) { } String idToken(String sessionId) { - OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri()) + OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri()) .subject(this.username).expiresAt(Instant.now().plusSeconds(86400)) .audience(List.of(this.registration.getClientId())).nonce(this.nonce) .claim(LogoutTokenClaimNames.SID, sessionId).build(); @@ -614,6 +682,13 @@ String idToken(String sessionId) { return this.encoder.encode(parameters).getTokenValue(); } + private String getIssuerUri() { + if (this.web == null) { + return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri(); + } + return this.web.url("/").toString(); + } + @GetMapping("/user") Map userinfo() { return Map.of("sub", this.username, "id", this.username); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index cb2ba0e137d..c247a6d7fed 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -82,7 +82,7 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 187c0979a2a..3957d416dae 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -42,7 +42,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index 97acdeaf05f..099207d0b5e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -26,13 +26,20 @@ import java.util.function.Consumer; import java.util.stream.Stream; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.ObservationTextPublisher; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -62,6 +69,7 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -95,8 +103,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken; public class WebSocketMessageBrokerSecurityConfigurationTests { @@ -381,6 +392,52 @@ public void sendMessageWhenAnonymousConfiguredAndAnonymousUserThenPasses() { clientInboundChannel().send(message("/anonymous")); } + @Test + public void sendMessageWhenObservationRegistryThenObserves() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + Message message = message(headers, "/authenticated"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + clientInboundChannel().send(message); + ObservationHandler observationHandler = this.context.getBean(ObservationHandler.class); + verify(observationHandler).onStart(any()); + verify(observationHandler).onStop(any()); + headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + message = message(headers, "/denyAll"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + try { + clientInboundChannel().send(message); + } + catch (MessageDeliveryException ex) { + // okay + } + verify(observationHandler).onError(any()); + } + + @Test + public void sendMessageWhenExcludeAuthorizationObservationsThenUnobserved() { + loadConfig(WebSocketSecurityConfig.class, ObservationRegistryConfig.class, SelectableObservationsConfig.class); + SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + Message message = message(headers, "/authenticated"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + clientInboundChannel().send(message); + ObservationHandler observationHandler = this.context.getBean(ObservationHandler.class); + headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); + headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE); + message = message(headers, "/denyAll"); + headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token); + try { + clientInboundChannel().send(message); + } + catch (MessageDeliveryException ex) { + // okay + } + verifyNoInteractions(observationHandler); + } + private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); @@ -892,4 +949,57 @@ static SyncExecutorSubscribableChannelPostProcessor postProcessor() { } + @Configuration + static class ObservationRegistryConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final ObservationHandler handler = spy(new ObservationTextPublisher()); + + @Bean + ObservationRegistry observationRegistry() { + return this.registry; + } + + @Bean + ObservationHandler observationHandler() { + return this.handler; + } + + @Bean + ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> handler) { + return new ObservationRegistryPostProcessor(handler); + } + + } + + static class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> handler; + + ObservationRegistryPostProcessor(ObjectProvider> handler) { + this.handler = handler; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry registry) { + registry.observationConfig().observationHandler(this.handler.getObject()); + } + return bean; + } + + } + + @Configuration + static class SelectableObservationsConfig { + + @Bean + SecurityObservationSettings observabilityDefaults() { + return SecurityObservationSettings.withDefaults().shouldObserveAuthorizations(false).build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java new file mode 100644 index 00000000000..75dd6c28779 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/observation/SecurityObservationSettingsTests.java @@ -0,0 +1,56 @@ +/* + * 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.observation; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecurityObservationSettings} + */ +public class SecurityObservationSettingsTests { + + @Test + void withDefaultsThenFilterOffAuthenticationOnAuthorizationOn() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults().build(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isTrue(); + assertThat(defaults.shouldObserveAuthorizations()).isTrue(); + } + + @Test + void noObservationsWhenConstructedThenAllOff() { + SecurityObservationSettings defaults = SecurityObservationSettings.noObservations(); + assertThat(defaults.shouldObserveRequests()).isFalse(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + + @Test + void withDefaultsWhenExclusionsThenInstanceReflects() { + SecurityObservationSettings defaults = SecurityObservationSettings.withDefaults() + .shouldObserveAuthentications(false) + .shouldObserveAuthorizations(false) + .shouldObserveRequests(true) + .build(); + assertThat(defaults.shouldObserveRequests()).isTrue(); + assertThat(defaults.shouldObserveAuthentications()).isFalse(); + assertThat(defaults.shouldObserveAuthorizations()).isFalse(); + } + +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java index 07d22be6172..63646400efd 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -29,7 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; -import org.springframework.core.ResolvableType; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.test.web.reactive.server.FluxExchangeResult; @@ -51,7 +51,6 @@ public class CorsSpecTests { @Mock private CorsConfigurationSource source; - @Mock private ApplicationContext context; ServerHttpSecurity http; @@ -62,6 +61,8 @@ public class CorsSpecTests { @BeforeEach public void setup() { + this.context = new GenericApplicationContext(); + ((GenericApplicationContext) this.context).refresh(); this.http = new TestingServerHttpSecurity().applicationContext(this.context); } @@ -92,9 +93,7 @@ public void corsWhenEnabledInLambdaThenAccessControlAllowOriginAndSecurityHeader @Test public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSecurityHeaders() { givenGetCorsConfigurationWillReturnWildcard(); - given(this.context.getBeanNamesForType(any(ResolvableType.class))).willReturn(new String[] { "source" }, - new String[0]); - given(this.context.getBean("source")).willReturn(this.source); + ((GenericApplicationContext) this.context).registerBean(CorsConfigurationSource.class, () -> this.source); this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); this.expectedHeaders.set("X-Frame-Options", "DENY"); assertHeaders(); @@ -102,7 +101,6 @@ public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSe @Test public void corsWhenNoConfigurationSourceThenNoCorsHeaders() { - given(this.context.getBeanNamesForType(any(ResolvableType.class))).willReturn(new String[0]); this.headerNamesNotPresent.add("Access-Control-Allow-Origin"); assertHeaders(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java index d348d95f8af..0bd8391d71d 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.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. @@ -17,9 +17,11 @@ package org.springframework.security.config.web.server; import java.net.URI; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; @@ -31,9 +33,12 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -41,8 +46,10 @@ import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; @@ -59,7 +66,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.server.ServerWebExchange; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -215,6 +224,62 @@ public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { verify(requestCache).getRedirectUri(any()); } + @Test + @SuppressWarnings("unchecked") + public void oauth2ClientWhenCustomAccessTokenResponseClientThenUsed() { + this.spring.register(OAuth2ClientBeanConfig.class, AuthorizedClientController.class).autowire(); + ReactiveClientRegistrationRepository clientRegistrationRepository = this.spring.getContext() + .getBean(ReactiveClientRegistrationRepository.class); + given(clientRegistrationRepository.findByRegistrationId(any())).willReturn(Mono.just(this.registration)); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext() + .getBean(ServerOAuth2AuthorizedClientRepository.class); + given(authorizedClientRepository.saveAuthorizedClient(any(OAuth2AuthorizedClient.class), + any(Authentication.class), any(ServerWebExchange.class))) + .willReturn(Mono.empty()); + ServerAuthorizationRequestRepository authorizationRequestRepository = this.spring + .getContext() + .getBean(ServerAuthorizationRequestRepository.class); + OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() + .redirectUri("/authorize/oauth2/code/registration-id") + .build(); + given(authorizationRequestRepository.loadAuthorizationRequest(any(ServerWebExchange.class))) + .willReturn(Mono.just(authorizationRequest)); + given(authorizationRequestRepository.removeAuthorizationRequest(any(ServerWebExchange.class))) + .willReturn(Mono.just(authorizationRequest)); + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = this.spring + .getContext() + .getBean(ReactiveOAuth2AccessTokenResponseClient.class); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("token") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .scopes(Set.of()) + .expiresIn(300) + .build(); + given(accessTokenResponseClient.getTokenResponse(any(OAuth2AuthorizationCodeGrantRequest.class))) + .willReturn(Mono.just(accessTokenResponse)); + // @formatter:off + this.client.get() + .uri((uriBuilder) -> uriBuilder + .path("/authorize/oauth2/code/registration-id") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + ) + .exchange() + .expectStatus().is3xxRedirection(); + // @formatter:on + ArgumentCaptor grantRequestArgumentCaptor = ArgumentCaptor + .forClass(OAuth2AuthorizationCodeGrantRequest.class); + verify(accessTokenResponseClient).getTokenResponse(grantRequestArgumentCaptor.capture()); + OAuth2AuthorizationCodeGrantRequest grantRequest = grantRequestArgumentCaptor.getValue(); + assertThat(grantRequest.getClientRegistration()).isEqualTo(this.registration); + assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationRequest()).isEqualTo(authorizationRequest); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode()).isEqualTo("code"); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getState()).isEqualTo("state"); + assertThat(grantRequest.getAuthorizationExchange().getAuthorizationResponse().getRedirectUri()) + .startsWith("/authorize/oauth2/code/registration-id"); + } + @Configuration @EnableWebFlux @EnableWebFluxSecurity @@ -324,4 +389,44 @@ SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { } + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class OAuth2ClientBeanConfig { + + @Bean + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + .oauth2Client((oauth2Client) -> oauth2Client + .authorizationRequestRepository(authorizationRequestRepository()) + ); + // @formatter:on + return http.build(); + } + + @Bean + @SuppressWarnings("unchecked") + ServerAuthorizationRequestRepository authorizationRequestRepository() { + return mock(ServerAuthorizationRequestRepository.class); + } + + @Bean + @SuppressWarnings("unchecked") + ReactiveOAuth2AccessTokenResponseClient authorizationCodeAccessTokenResponseClient() { + return mock(ReactiveOAuth2AccessTokenResponseClient.class); + } + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository() { + return mock(ReactiveClientRegistrationRepository.class); + } + + @Bean + ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { + return mock(ServerOAuth2AuthorizedClientRepository.class); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java index 7e24cd3816f..8514528aa78 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java @@ -75,6 +75,8 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoderParameters; @@ -371,6 +373,30 @@ void logoutWhenCustomComponentsThenUses() { verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class)); } + @Test + void logoutWhenProviderIssuerMissingThen5xxServerError() { + this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class) + .autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + String session = login(); + String logoutToken = this.test.mutateWith(session(session)) + .get() + .uri("/token/logout") + .exchange() + .expectStatus() + .isOk() + .returnResult(String.class) + .getResponseBody() + .blockFirst(); + this.test.post() + .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .body(BodyInserters.fromFormData("logout_token", logoutToken)) + .exchange() + .expectStatus() + .is5xxServerError(); + this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isOk(); + } + private String login() { this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized(); String registrationId = this.clientRegistration.getRegistrationId(); @@ -624,6 +650,54 @@ ServerLogoutHandler logoutHandler() { } + @Configuration + static class ProviderIssuerMissingRegistrationConfig { + + @Autowired(required = false) + MockWebServer web; + + @Bean + ClientRegistration clientRegistration() { + if (this.web == null) { + return TestClientRegistrations.clientRegistration().issuerUri(null).build(); + } + String issuer = this.web.url("/").toString(); + return TestClientRegistrations.clientRegistration() + .issuerUri(null) + .jwkSetUri(issuer + "jwks") + .tokenUri(issuer + "token") + .userInfoUri(issuer + "user") + .scope("openid") + .build(); + } + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) { + return new InMemoryReactiveClientRegistrationRepository(clientRegistration); + } + + } + + @Configuration + @EnableWebFluxSecurity + @Import(ProviderIssuerMissingRegistrationConfig.class) + static class ProviderIssuerMissingConfig { + + @Bean + @Order(1) + SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); + // @formatter:on + + return http.build(); + } + + } + @Configuration @EnableWebFluxSecurity @EnableWebFlux @@ -662,6 +736,9 @@ private static JWKSource jwks(RSAKey key) { @Autowired ClientRegistration registration; + @Autowired(required = false) + MockWebServer web; + static ServerWebExchangeMatcher or(String... patterns) { List matchers = new ArrayList<>(); for (String pattern : patterns) { @@ -706,7 +783,7 @@ String nonce(@RequestParam("nonce") String nonce, @RequestParam("state") String Map accessToken(WebSession session) { JwtEncoderParameters parameters = JwtEncoderParameters .from(JwtClaimsSet.builder().id("id").subject(this.username) - .issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now()) + .issuer(getIssuerUri()).issuedAt(Instant.now()) .expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build()); String token = this.encoder.encode(parameters).getTokenValue(); return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null) @@ -714,7 +791,7 @@ Map accessToken(WebSession session) { } String idToken(String sessionId) { - OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri()) + OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri()) .subject(this.username).expiresAt(Instant.now().plusSeconds(86400)) .audience(List.of(this.registration.getClientId())).nonce(this.nonce) .claim(LogoutTokenClaimNames.SID, sessionId).build(); @@ -723,6 +800,13 @@ String idToken(String sessionId) { return this.encoder.encode(parameters).getTokenValue(); } + private String getIssuerUri() { + if (this.web == null) { + return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri(); + } + return this.web.url("/").toString(); + } + @GetMapping("/user") Map userinfo() { return Map.of("sub", this.username, "id", this.username); @@ -737,8 +821,9 @@ String jwks() { String logoutToken(@AuthenticationPrincipal OidcUser user) { OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) .audience(List.of(this.registration.getClientId())).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); + JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256).type("logout+jwt").build(); + JwtClaimsSet claims = JwtClaimsSet.builder().claims((c) -> c.putAll(token.getClaims())).build(); + JwtEncoderParameters parameters = JwtEncoderParameters.from(header, claims); return this.encoder.encode(parameters).getTokenValue(); } @@ -747,8 +832,9 @@ String logoutTokenAll(@AuthenticationPrincipal OidcUser user) { OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) .audience(List.of(this.registration.getClientId())) .claims((claims) -> claims.remove(LogoutTokenClaimNames.SID)).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); + JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256).type("JWT").build(); + JwtClaimsSet claims = JwtClaimsSet.builder().claims((c) -> c.putAll(token.getClaims())).build(); + JwtEncoderParameters parameters = JwtEncoderParameters.from(header, claims); return this.encoder.encode(parameters).getTokenValue(); } } diff --git a/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java b/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java index 9683ca49842..6365bdb5f1d 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java @@ -75,7 +75,12 @@ private boolean isExpired(OneTimeToken ott) { return this.clock.instant().isAfter(ott.getExpiresAt()); } - void setClock(Clock clock) { + /** + * Sets the {@link Clock} used when generating one-time token and checking token + * expiry. + * @param clock the clock + */ + public void setClock(Clock clock) { Assert.notNull(clock, "clock cannot be null"); this.clock = clock; } diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java index d2809673bfb..76be0842789 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateSecurityAnnotationScanner.java @@ -25,7 +25,6 @@ import java.util.Map; import java.util.Set; -import org.springframework.core.MethodClassKey; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -80,10 +79,6 @@ final class ExpressionTemplateSecurityAnnotationScanner private final AnnotationTemplateExpressionDefaults templateDefaults; - private final Map> uniqueParameterAnnotationCache = new HashMap<>(); - - private final Map> uniqueMethodAnnotationCache = new HashMap<>(); - ExpressionTemplateSecurityAnnotationScanner(Class type, AnnotationTemplateExpressionDefaults templateDefaults) { Assert.notNull(type, "type cannot be null"); Assert.notNull(templateDefaults, "templateDefaults cannot be null"); @@ -95,17 +90,14 @@ final class ExpressionTemplateSecurityAnnotationScanner @Override MergedAnnotation merge(AnnotatedElement element, Class targetClass) { if (element instanceof Parameter parameter) { - MergedAnnotation annotation = this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, - (p) -> this.unique.merge(p, targetClass)); + MergedAnnotation annotation = this.unique.merge(parameter, targetClass); if (annotation == null) { return null; } return resolvePlaceholders(annotation); } if (element instanceof Method method) { - MethodClassKey key = new MethodClassKey(method, targetClass); - MergedAnnotation annotation = this.uniqueMethodAnnotationCache.computeIfAbsent(key, - (k) -> this.unique.merge(method, targetClass)); + MergedAnnotation annotation = this.unique.merge(method, targetClass); if (annotation == null) { return null; } @@ -135,10 +127,9 @@ private MergedAnnotation resolvePlaceholders(MergedAnnotation mergedAnnota } Map annotationProperties = mergedAnnotation.asMap(); for (Map.Entry annotationProperty : annotationProperties.entrySet()) { - if (!(annotationProperty.getValue() instanceof String)) { + if (!(annotationProperty.getValue() instanceof String expression)) { continue; } - String expression = (String) annotationProperty.getValue(); String value = helper.replacePlaceholders(expression, stringProperties::get); properties.put(annotationProperty.getKey(), value); } diff --git a/core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java b/core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java index 23b398708b6..8e76c1538c8 100644 --- a/core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNoException; /** @@ -100,6 +101,15 @@ void generateWhenMoreThan100TokensThenClearExpired() { // @formatter:on } + @Test + void setClockWhenNullThenThrowIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.oneTimeTokenService.setClock(null)) + .withMessage("clock cannot be null"); + // @formatter:on + } + private List generate(int howMany) { List generated = new ArrayList<>(howMany); for (int i = 0; i < howMany; i++) { diff --git a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java index a9a1f2ea0a5..9791c2daf5a 100644 --- a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java @@ -18,10 +18,18 @@ import java.util.Collection; import java.util.Properties; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; @@ -31,6 +39,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -151,6 +160,36 @@ public void loadUserByUsernameWhenInstanceOfCredentialsContainerThenReturnInstan assertThat(manager.loadUserByUsername(user.getUsername())).isSameAs(user); } + @ParameterizedTest + @MethodSource("authenticationErrorCases") + void authenticateWhenInvalidMissingOrMalformedIdThenException(String username, String password, + String expectedMessage) { + UserDetails user = User.builder().username(username).password(password).roles("USER").build(); + InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager(user); + + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userManager); + authenticationProvider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); + + AuthenticationManager authManager = new ProviderManager(authenticationProvider); + + assertThatIllegalArgumentException() + .isThrownBy(() -> authManager.authenticate(new UsernamePasswordAuthenticationToken(username, "password"))) + .withMessage(expectedMessage); + } + + private static Stream authenticationErrorCases() { + return Stream.of(Arguments + .of("user", "password", "Given that there is no default password encoder configured, each " + + "password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`."), + Arguments.of("user", "bycrpt}password", + "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."), + Arguments.of("user", "{bycrptpassword", + "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."), + Arguments.of("user", "{ren&stimpy}password", + "There is no password encoder mapped for the id 'ren&stimpy'. Check your configuration to ensure it matches one of the registered encoders.")); + } + static class CustomUser implements MutableUserDetails, CredentialsContainer { private final UserDetails delegate; diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index 22711242f2b..a7bdc66073e 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -19,6 +19,8 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.util.StringUtils; + /** * A password encoder that delegates to another PasswordEncoder based upon a prefixed * identifier. @@ -129,9 +131,14 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { private static final String DEFAULT_ID_SUFFIX = "}"; - public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\""; + private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. " + + "Check your configuration to ensure it matches one of the registered encoders."; + + private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, each password must have a password encoding prefix. " + + "Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`."; - public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`."; + private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly " + + "formatted or incomplete. The format should be '%sENCODER%spassword'."; private final String idPrefix; @@ -290,10 +297,18 @@ public String encode(CharSequence rawPassword) { @Override public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { String id = extractId(prefixEncodedPassword); - if (id != null && !id.isEmpty()) { + if (StringUtils.hasText(id)) { throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id)); } - throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX); + if (StringUtils.hasText(prefixEncodedPassword)) { + int start = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idPrefix); + int end = prefixEncodedPassword.indexOf(DelegatingPasswordEncoder.this.idSuffix, start); + if (start < 0 && end < 0) { + throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX); + } + } + throw new IllegalArgumentException(String.format(MALFORMED_PASSWORD_ENCODER_PREFIX, + DelegatingPasswordEncoder.this.idPrefix, DelegatingPasswordEncoder.this.idSuffix)); } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index a3222fb41ea..2ac32d8a53a 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -43,8 +43,6 @@ @ExtendWith(MockitoExtension.class) public class DelegatingPasswordEncoderTests { - public static final String NO_PASSWORD_ENCODER = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`."; - @Mock private PasswordEncoder bcrypt; @@ -70,6 +68,14 @@ public class DelegatingPasswordEncoderTests { private DelegatingPasswordEncoder onlySuffixPasswordEncoder; + private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id 'unmapped'. " + + "Check your configuration to ensure it matches one of the registered encoders."; + + private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, " + + "each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`."; + + private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly formatted or incomplete. The format should be '{ENCODER}password'."; + @BeforeEach public void setup() { this.delegates = new HashMap<>(); @@ -195,7 +201,7 @@ public void matchesWhenNoopThenDelegatesToNoop() { public void matchesWhenUnMappedThenIllegalArgumentException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{unmapped}" + this.rawPassword)) - .withMessage("There is no PasswordEncoder mapped for the id \"unmapped\""); + .withMessage(NO_PASSWORD_ENCODER_MAPPED); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -203,7 +209,7 @@ public void matchesWhenUnMappedThenIllegalArgumentException() { public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{bcrypt" + this.rawPassword)) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -211,7 +217,7 @@ public void matchesWhenNoClosingPrefixStringThenIllegalArgumentException() { public void matchesWhenNoStartingPrefixStringThenFalse() { assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "bcrypt}" + this.rawPassword)) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -219,7 +225,7 @@ public void matchesWhenNoStartingPrefixStringThenFalse() { public void matchesWhenNoIdStringThenFalse() { assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "{}" + this.rawPassword)) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -228,7 +234,7 @@ public void matchesWhenPrefixInMiddleThenFalse() { assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches(this.rawPassword, "invalid" + this.bcryptEncodedPassword)) .isInstanceOf(IllegalArgumentException.class) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(MALFORMED_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -238,7 +244,7 @@ public void matchesWhenIdIsNullThenFalse() { DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates); assertThatIllegalArgumentException() .isThrownBy(() -> passwordEncoder.matches(this.rawPassword, this.rawPassword)) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(NO_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); } @@ -296,9 +302,8 @@ void matchesShouldThrowIllegalArgumentExceptionWhenNoPasswordEncoderIsMappedForT assertThatIllegalArgumentException() .isThrownBy(() -> this.passwordEncoder.matches("rawPassword", "prefixEncodedPassword")) .isInstanceOf(IllegalArgumentException.class) - .withMessage(NO_PASSWORD_ENCODER); + .withMessage(NO_PASSWORD_ENCODER_PREFIX); verifyNoMoreInteractions(this.bcrypt, this.noop); - } } diff --git a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc index 0534fae5820..2b0db4865ee 100644 --- a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc @@ -187,7 +187,7 @@ Xml:: If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. However, this may turn off observations for more than just Spring Security. -Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: +Instead, you can publish a `SecurityObservationSettings` like the following: [tabs] ====== @@ -196,9 +196,8 @@ Java:: [source,java,role="primary"] ---- @Bean -ObservationRegistryCustomizer noSpringSecurityObservations() { - ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); +SecurityObservationSettings noSpringSecurityObservations() { + return SecurityObservationSettings.noObservations(); } ---- @@ -207,17 +206,77 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Bean -fun noSpringSecurityObservations(): ObservationRegistryCustomizer { - ObservationPredicate predicate = (name: String, context: Observation.Context) -> !name.startsWith("spring.security.") - (registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate) +fun noSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.noObservations() } ---- ====== +and then Spring Security will not wrap any filter chains, authentications, or authorizations in their `ObservationXXX` counterparts. + [TIP] There is no facility for disabling observations with XML support. Instead, simply do not set the `observation-registry-ref` attribute. +You can also disable security for only a subset of Security's observations. +For example, the `SecurityObservationSettings` bean excludes the filter chain observations by default. +So, you can also do: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings defaultSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults().build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun defaultSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.withDefaults().build() +} +---- +====== + +Or you can turn on and off observations individually, based on the defaults: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings allSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults() + .shouldObserveFilterChains(true).build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun allSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservabilityDefaults.builder() + .shouldObserveFilterChains(true).build() +} +---- +====== + +[NOTE] +===== +For backward compatibility, all Spring Security observations are made unless a `SecurityObservationSettings` is published. +===== + [[webflux-observability-tracing-listing]] === Trace Listing diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc index 207ddb76549..5b708524c9d 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc @@ -26,7 +26,7 @@ image:{icondir}/number_4.png[] The browser requests the login page to which it w image:{icondir}/number_5.png[] Something within the application, must <>. [[servlet-authentication-usernamepasswordauthenticationfilter]] -When the username and password are submitted, the `UsernamePasswordAuthenticationFilter` authenticates the username and password. +When the username and password are submitted, the `UsernamePasswordAuthenticationFilter` creates a `UsernamePasswordAuthenticationToken` which is a type of https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-authentication[Authentication], by extracting the username and password from the `HttpServletRequest` instance. The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so the following diagram should look pretty similar: .Authenticating Username and Password diff --git a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc index 0a43f504664..d6a8072f249 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/persistence.adoc @@ -197,13 +197,14 @@ image:{icondir}/number_1.png[] Before running the rest of the application, `Secu image:{icondir}/number_2.png[] Next, the application is ran. -image:{icondir}/number_3.png[] Finally, if the `SecurityContext` has changed, we save the `SecurityContext` using the `SecurityContextPersistenceRepository`. +image:{icondir}/number_3.png[] Finally, if the `SecurityContext` has changed, we save the `SecurityContext` using the `SecurityContextRepository`. This means that when using `SecurityContextPersistenceFilter`, just setting the `SecurityContextHolder` will ensure that the `SecurityContext` is persisted using `SecurityContextRepository`. In some cases a response is committed and written to the client before the `SecurityContextPersistenceFilter` method completes. For example, if a redirect is sent to the client the response is immediately written back to the client. This means that establishing an `HttpSession` would not be possible in step 3 because the session id could not be included in the already written response. -Another situation that can happen is that if a client authenticates successfully, the response is committed before `SecurityContextPersistenceFilter` completes, and the client makes a second request before the `SecurityContextPersistenceFilter` completes the wrong authentication could be present in the second request. +Another situation that can happen is that if a client authenticates successfully, the response is committed before `SecurityContextPersistenceFilter` completes, and the client makes a second request before the `SecurityContextPersistenceFilter` completes. the wrong authentication could be present in the second request. + To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `HttpServletRequest` and the `HttpServletResponse` to detect if the `SecurityContext` has changed and if so save the `SecurityContext` just before the response is committed. diff --git a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc index 07df0c3ad55..26e2ac3cdd4 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc @@ -101,7 +101,7 @@ The `AuthorizationManager` interface contains two methods: ---- AuthorizationDecision check(Supplier authentication, Object secureObject); -default AuthorizationDecision verify(Supplier authentication, Object secureObject) +default void verify(Supplier authentication, Object secureObject) throws AccessDeniedException { // ... } diff --git a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc index b757a862f29..e9d5f33090c 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc @@ -192,7 +192,7 @@ Xml:: If you don't want any Spring Security observations, in a Spring Boot application you can publish a `ObservationRegistry.NOOP` `@Bean`. However, this may turn off observations for more than just Spring Security. -Instead, you can alter the provided `ObservationRegistry` with an `ObservationPredicate` like the following: +Instead, you can publish a `SecurityObservationSettings` like the following: [tabs] ====== @@ -201,9 +201,8 @@ Java:: [source,java,role="primary"] ---- @Bean -ObservationRegistryCustomizer noSpringSecurityObservations() { - ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); +SecurityObservationSettings noSpringSecurityObservations() { + return SecurityObservationSettings.noObservations(); } ---- @@ -212,17 +211,77 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Bean -fun noSpringSecurityObservations(): ObservationRegistryCustomizer { - ObservationPredicate predicate = (name: String, context: Observation.Context) -> !name.startsWith("spring.security.") - (registry: ObservationRegistry) -> registry.observationConfig().observationPredicate(predicate) +fun noSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.noObservations() } ---- ====== +and then Spring Security will not wrap any filter chains, authentications, or authorizations in their `ObservationXXX` counterparts. + [TIP] There is no facility for disabling observations with XML support. Instead, simply do not set the `observation-registry-ref` attribute. +You can also disable security for only a subset of Security's observations. +For example, the `SecurityObservationSettings` bean excludes the filter chain observations by default. +So, you can also do: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings defaultSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults().build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun defaultSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.withDefaults().build() +} +---- +====== + +Or you can turn on and off observations individually, based on the defaults: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityObservationSettings allSpringSecurityObservations() { + return SecurityObservationSettings.withDefaults() + .shouldObserveFilterChains(true).build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun allSpringSecurityObservations(): SecurityObservationSettings { + return SecurityObservationSettings.builder() + .shouldObserveFilterChains(true).build() +} +---- +====== + +[NOTE] +===== +For backward compatibility, the all Spring Security observations are made unless a `SecurityObservationSettings` is published. +===== + [[observability-tracing-listing]] === Trace Listing diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index 048f9b0b55f..efd3763e8c8 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -365,6 +365,15 @@ Java:: @Configuration public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { + private final ApplicationContext applicationContext; + + private final AuthorizationManager> authorizationManager; + + public WebSocketSecurityConfig(ApplicationContext applicationContext, AuthorizationManager> authorizationManager) { + this.applicationContext = applicationContext; + this.authorizationManager = authorizationManager; + } + @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); @@ -372,9 +381,8 @@ public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer @Override public void configureClientInboundChannel(ChannelRegistration registration) { - AuthorizationManager> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated(); - AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules); - AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context); + AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(authorizationManager); + AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(applicationContext); authz.setAuthorizationEventPublisher(publisher); registration.interceptors(new SecurityContextChannelInterceptor(), authz); } @@ -386,7 +394,7 @@ Kotlin:: [source,kotlin,role="secondary"] ---- @Configuration -open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer { +open class WebSocketSecurityConfig(val applicationContext: ApplicationContext, val authorizationManager: AuthorizationManager>) : WebSocketMessageBrokerConfigurer { @Override override fun addArgumentResolvers(argumentResolvers: List) { argumentResolvers.add(AuthenticationPrincipalArgumentResolver()) @@ -394,9 +402,8 @@ open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer { @Override override fun configureClientInboundChannel(registration: ChannelRegistration) { - var myAuthorizationRules: AuthorizationManager> = AuthenticatedAuthorizationManager.authenticated() - var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules) - var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context) + var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(authorizationManager) + var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(applicationContext) authz.setAuthorizationEventPublisher(publisher) registration.interceptors(SecurityContextChannelInterceptor(), authz) } diff --git a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc index 60a34b84a85..6dcb167e858 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc @@ -816,7 +816,7 @@ public class RestClientConfig { private static ClientRegistrationIdResolver clientRegistrationIdResolver() { return (request) -> { - Authentication authentication = SecurityContextHolder.getAuthentication(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return (authentication instanceof OAuth2AuthenticationToken principal) ? principal.getAuthorizedClientRegistrationId() : null; diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index 8e2b765cd6e..187a0579c14 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -89,12 +89,12 @@ Next, let's see the architectural components that Spring Security uses to suppor javadoc:org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider[] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> and <> to authenticate a JWT. Let's take a look at how `JwtAuthenticationProvider` works within Spring Security. -The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. +The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works. .`JwtAuthenticationProvider` Usage image::{figures}/jwtauthenticationprovider.png[] -image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. +image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `JwtAuthenticationProvider`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index 90b64a01f46..5a4df2eba8a 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -85,12 +85,12 @@ Next, let's see the architectural components that Spring Security uses to suppor javadoc:org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider[] is an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[`AuthenticationProvider`] implementation that leverages a <> to authenticate an opaque token. Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security. -The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <> works. +The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] works. .`OpaqueTokenAuthenticationProvider` Usage image::{figures}/opaquetokenauthenticationprovider.png[] -image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. +image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/oauth2/resource-server/index.adoc#oauth2resourceserver-authentication-bearertokenauthenticationfilter[Reading the Bearer Token] passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationprovider[AuthenticationProvider] of type `OpaqueTokenAuthenticationProvider`. diff --git a/docs/package.json b/docs/package.json index 1b6f7bbf568..78c6f3c48ab 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,6 +5,6 @@ "@antora/collector-extension": "1.0.0-beta.2", "@asciidoctor/tabs": "1.0.0-beta.6", "@springio/antora-extensions": "1.14.2", - "@springio/asciidoctor-extensions": "1.0.0-alpha.13" + "@springio/asciidoctor-extensions": "1.0.0-alpha.14" } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81300910a6d..25b3dddfec1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,14 +11,14 @@ org-bouncycastle = "1.78.1" org-eclipse-jetty = "11.0.24" org-jetbrains-kotlin = "1.9.25" org-jetbrains-kotlinx = "1.9.0" -org-mockito = "5.13.0" +org-mockito = "5.14.1" org-opensaml = "4.3.2" org-opensaml5 = "5.1.2" org-springframework = "6.2.0-RC1" [libraries] ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.5.8" -com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.17.2" +com-fasterxml-jackson-jackson-bom = "com.fasterxml.jackson:jackson-bom:2.18.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" @@ -28,7 +28,7 @@ com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version. com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11" com-unboundid-unboundid-ldapsdk7 = "com.unboundid:unboundid-ldapsdk:7.0.1" commons-collections = "commons-collections:commons-collections:3.2.2" -io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.13.4" +io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.13.5" io-mockk = "io.mockk:mockk:1.13.12" io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.10" io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" } @@ -70,12 +70,12 @@ org-bouncycastle-bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk18on", 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.6.0.Final" +org-hibernate-orm-hibernate-core = "org.hibernate.orm:hibernate-core:6.6.1.Final" org-hsqldb = "org.hsqldb:hsqldb:2.7.3" 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.25" org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } -org-junit-junit-bom = "org.junit:junit-bom:5.11.0" +org-junit-junit-bom = "org.junit:junit-bom:5.11.1" org-mockito-mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "org-mockito" } org-opensaml-opensaml-saml-api = { module = "org.opensaml:opensaml-saml-api", version.ref = "org-opensaml" } org-opensaml-opensaml-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml" } @@ -83,7 +83,7 @@ org-opensaml-opensaml5-saml-api = { module = "org.opensaml:opensaml-saml-api", v org-opensaml-opensaml5-saml-impl = { module = "org.opensaml:opensaml-saml-impl", version.ref = "org-opensaml5" } org-python-jython = { module = "org.python:jython", version = "2.5.3" } org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit3-driver:4.23.0" -org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.24.0" +org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:4.25.0" org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59" org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3" org-slf4j-log4j-over-slf4j = "org.slf4j:log4j-over-slf4j:1.7.36" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2b189974c29..8e876e1c557 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java index 256ced675ab..ca22416af95 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java @@ -90,7 +90,7 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, grantRequest); return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), - tokenResponse.getAccessToken()); + tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); } private OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java index 43e0607d2ea..b3791a5da04 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java @@ -88,7 +88,7 @@ public Mono authorize(OAuth2AuthorizationContext context .onErrorMap(OAuth2AuthorizationException.class, (ex) -> new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex)) .map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), - tokenResponse.getAccessToken())); + tokenResponse.getAccessToken(), tokenResponse.getRefreshToken())); } private Mono resolveSubjectToken(OAuth2AuthorizationContext context) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java index 2a6128d9711..1fe4b704ce6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java @@ -34,8 +34,6 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.oauth2.client.ClientAuthorizationException; import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; @@ -121,16 +119,15 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque private final OAuth2AuthorizedClientManager authorizedClientManager; - private final ClientRegistrationIdResolver clientRegistrationIdResolver; + private ClientRegistrationIdResolver clientRegistrationIdResolver = new RequestAttributeClientRegistrationIdResolver(); + + private PrincipalResolver principalResolver = new SecurityContextHolderPrincipalResolver(); // @formatter:off private OAuth2AuthorizationFailureHandler authorizationFailureHandler = (clientRegistrationId, principal, attributes) -> { }; // @formatter:on - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - /** * Constructs a {@code OAuth2ClientHttpRequestInterceptor} using the provided * parameters. @@ -138,23 +135,8 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque * manages the authorized client(s) */ public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager) { - this(authorizedClientManager, new RequestAttributeClientRegistrationIdResolver()); - } - - /** - * Constructs a {@code OAuth2ClientHttpRequestInterceptor} using the provided - * parameters. - * @param authorizedClientManager the {@link OAuth2AuthorizedClientManager} which - * manages the authorized client(s) - * @param clientRegistrationIdResolver the strategy for resolving a - * {@code clientRegistrationId} from the intercepted request - */ - public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager, - ClientRegistrationIdResolver clientRegistrationIdResolver) { Assert.notNull(authorizedClientManager, "authorizedClientManager cannot be null"); - Assert.notNull(clientRegistrationIdResolver, "clientRegistrationIdResolver cannot be null"); this.authorizedClientManager = authorizedClientManager; - this.clientRegistrationIdResolver = clientRegistrationIdResolver; } /** @@ -238,20 +220,31 @@ public static OAuth2AuthorizationFailureHandler authorizationFailureHandler( } /** - * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use - * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}. - * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to - * use + * Sets the strategy for resolving a {@code clientRegistrationId} from an intercepted + * request. + * @param clientRegistrationIdResolver the strategy for resolving a + * {@code clientRegistrationId} from an intercepted request + */ + public void setClientRegistrationIdResolver(ClientRegistrationIdResolver clientRegistrationIdResolver) { + Assert.notNull(clientRegistrationIdResolver, "clientRegistrationIdResolver cannot be null"); + this.clientRegistrationIdResolver = clientRegistrationIdResolver; + } + + /** + * Sets the strategy for resolving a {@link Authentication principal} from an + * intercepted request. + * @param principalResolver the strategy for resolving a {@link Authentication + * principal} */ - public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { - Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); - this.securityContextHolderStrategy = securityContextHolderStrategy; + public void setPrincipalResolver(PrincipalResolver principalResolver) { + Assert.notNull(principalResolver, "principalResolver cannot be null"); + this.principalResolver = principalResolver; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - Authentication principal = this.securityContextHolderStrategy.getContext().getAuthentication(); + Authentication principal = this.principalResolver.resolve(request); if (principal == null) { principal = ANONYMOUS_AUTHENTICATION; } @@ -378,4 +371,24 @@ public interface ClientRegistrationIdResolver { } + /** + * A strategy for resolving a {@link Authentication principal} from an intercepted + * request. + */ + @FunctionalInterface + public interface PrincipalResolver { + + /** + * Resolve the {@link Authentication principal} from the current request, which is + * used to obtain an {@link OAuth2AuthorizedClient}. + * @param request the intercepted request, containing HTTP method, URI, headers, + * and request attributes + * @return the {@link Authentication principal} to be used for resolving an + * {@link OAuth2AuthorizedClient}. + */ + @Nullable + Authentication resolve(HttpRequest request); + + } + } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java index 9e8f7b51a0f..f1031ae9c18 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java @@ -30,6 +30,7 @@ * using {@link ClientHttpRequest#getAttributes() attributes}. * * @author Steve Riesenberg + * @since 6.4 * @see OAuth2ClientHttpRequestInterceptor */ public final class RequestAttributeClientRegistrationIdResolver diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java new file mode 100644 index 00000000000..bbae6e86c0a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java @@ -0,0 +1,88 @@ +/* + * 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.oauth2.client.web.client; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.util.Assert; + +/** + * A strategy for resolving a {@link Authentication principal} from an intercepted request + * using {@link ClientHttpRequest#getAttributes() attributes}. + * + * @author Steve Riesenberg + * @since 6.4 + */ +public class RequestAttributePrincipalResolver implements OAuth2ClientHttpRequestInterceptor.PrincipalResolver { + + private static final String PRINCIPAL_ATTR_NAME = RequestAttributePrincipalResolver.class.getName() + .concat(".principal"); + + @Override + public Authentication resolve(HttpRequest request) { + return (Authentication) request.getAttributes().get(PRINCIPAL_ATTR_NAME); + } + + /** + * Modifies the {@link ClientHttpRequest#getAttributes() attributes} to include the + * {@link Authentication principal} to be used to look up the + * {@link OAuth2AuthorizedClient}. + * @param principal the {@link Authentication principal} to be used to look up the + * {@link OAuth2AuthorizedClient} + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> principal(Authentication principal) { + Assert.notNull(principal, "principal cannot be null"); + return (attributes) -> attributes.put(PRINCIPAL_ATTR_NAME, principal); + } + + /** + * Modifies the {@link ClientHttpRequest#getAttributes() attributes} to include the + * {@link Authentication principal} to be used to look up the + * {@link OAuth2AuthorizedClient}. + * @param principalName the {@code principalName} to be used to look up the + * {@link OAuth2AuthorizedClient} + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> principal(String principalName) { + Assert.hasText(principalName, "principalName cannot be empty"); + Authentication principal = createAuthentication(principalName); + return (attributes) -> attributes.put(PRINCIPAL_ATTR_NAME, principal); + } + + private static Authentication createAuthentication(String principalName) { + return new AbstractAuthenticationToken(Collections.emptySet()) { + @Override + public Object getPrincipal() { + return principalName; + } + + @Override + public Object getCredentials() { + return null; + } + }; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java new file mode 100644 index 00000000000..8ffd5b204c4 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java @@ -0,0 +1,57 @@ +/* + * 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.oauth2.client.web.client; + +import org.springframework.http.HttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; + +/** + * A strategy for resolving a {@link Authentication principal} from an intercepted request + * using the {@link SecurityContextHolder}. + * + * @author Steve Riesenberg + * @since 6.4 + */ +public class SecurityContextHolderPrincipalResolver implements OAuth2ClientHttpRequestInterceptor.PrincipalResolver { + + private final SecurityContextHolderStrategy securityContextHolderStrategy; + + /** + * Constructs a {@code SecurityContextHolderPrincipalResolver}. + */ + public SecurityContextHolderPrincipalResolver() { + this(SecurityContextHolder.getContextHolderStrategy()); + } + + /** + * Constructs a {@code SecurityContextHolderPrincipalResolver} using the provided + * parameters. + * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to + * use for resolving the {@link Authentication principal} + */ + public SecurityContextHolderPrincipalResolver(SecurityContextHolderStrategy securityContextHolderStrategy) { + this.securityContextHolderStrategy = securityContextHolderStrategy; + } + + @Override + public Authentication resolve(HttpRequest request) { + return this.securityContextHolderStrategy.getContext().getAuthentication(); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProviderTests.java index 8cf3b0fdf0f..ddc9ead28df 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProviderTests.java @@ -213,7 +213,9 @@ public void authorizeWhenTokenExchangeAndTokenExpiredThenReauthorized() { issuedAt, expiresAt); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(), accessToken); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(accessTokenResponse); // @formatter:off @@ -228,6 +230,7 @@ public void authorizeWhenTokenExchangeAndTokenExpiredThenReauthorized() { assertThat(reauthorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(reauthorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(reauthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(reauthorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -248,7 +251,9 @@ public void authorizeWhenTokenExchangeAndTokenNotExpiredButClockSkewForcesExpiry // Shorten the lifespan of the access token by 90 seconds, which will ultimately // force it to expire on the client this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(90)); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(accessTokenResponse); // @formatter:off @@ -263,6 +268,7 @@ public void authorizeWhenTokenExchangeAndTokenNotExpiredButClockSkewForcesExpiry assertThat(reauthorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(reauthorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(reauthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(reauthorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -285,7 +291,9 @@ public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenDoesNotReso @Test public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenResolvesThenAuthorized() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(accessTokenResponse); // @formatter:off @@ -299,6 +307,7 @@ public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenResolvesThe assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -312,7 +321,9 @@ public void authorizeWhenCustomSubjectTokenResolverSetThenCalled() { Function subjectTokenResolver = mock(Function.class); given(subjectTokenResolver.apply(any(OAuth2AuthorizationContext.class))).willReturn(this.subjectToken); this.authorizedClientProvider.setSubjectTokenResolver(subjectTokenResolver); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(accessTokenResponse); TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); @@ -327,6 +338,7 @@ public void authorizeWhenCustomSubjectTokenResolverSetThenCalled() { assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); verify(subjectTokenResolver).apply(authorizationContext); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); @@ -341,7 +353,9 @@ public void authorizeWhenCustomActorTokenResolverSetThenCalled() { Function actorTokenResolver = mock(Function.class); given(actorTokenResolver.apply(any(OAuth2AuthorizationContext.class))).willReturn(this.actorToken); this.authorizedClientProvider.setActorTokenResolver(actorTokenResolver); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(accessTokenResponse); // @formatter:off @@ -355,6 +369,7 @@ public void authorizeWhenCustomActorTokenResolverSetThenCalled() { assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); verify(actorTokenResolver).apply(authorizationContext); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProviderTests.java index 2b7250911f7..99787f163e1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProviderTests.java @@ -215,7 +215,9 @@ public void authorizeWhenTokenExchangeAndTokenExpiredThenReauthorized() { issuedAt, expiresAt); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(), accessToken); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -231,6 +233,7 @@ public void authorizeWhenTokenExchangeAndTokenExpiredThenReauthorized() { assertThat(reauthorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(reauthorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(reauthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(reauthorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -251,7 +254,9 @@ public void authorizeWhenTokenExchangeAndTokenNotExpiredButClockSkewForcesExpiry // Shorten the lifespan of the access token by 90 seconds, which will ultimately // force it to expire on the client this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(90)); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -267,6 +272,7 @@ public void authorizeWhenTokenExchangeAndTokenNotExpiredButClockSkewForcesExpiry assertThat(reauthorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(reauthorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(reauthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(reauthorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -289,7 +295,9 @@ public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenDoesNotReso @Test public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenResolvesThenAuthorized() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -303,6 +311,7 @@ public void authorizeWhenTokenExchangeAndNotAuthorizedAndSubjectTokenResolvesThe assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); verify(this.accessTokenResponseClient).getTokenResponse(grantRequestCaptor.capture()); @@ -317,7 +326,9 @@ public void authorizeWhenCustomSubjectTokenResolverSetThenCalled() { given(subjectTokenResolver.apply(any(OAuth2AuthorizationContext.class))) .willReturn(Mono.just(this.subjectToken)); this.authorizedClientProvider.setSubjectTokenResolver(subjectTokenResolver); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(Mono.just(accessTokenResponse)); TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); @@ -332,6 +343,7 @@ public void authorizeWhenCustomSubjectTokenResolverSetThenCalled() { assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); verify(subjectTokenResolver).apply(authorizationContext); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); @@ -346,7 +358,9 @@ public void authorizeWhenCustomActorTokenResolverSetThenCalled() { Function> actorTokenResolver = mock(Function.class); given(actorTokenResolver.apply(any(OAuth2AuthorizationContext.class))).willReturn(Mono.just(this.actorToken)); this.authorizedClientProvider.setActorTokenResolver(actorTokenResolver); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse() + .refreshToken("refresh") + .build(); given(this.accessTokenResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) .willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -360,6 +374,7 @@ public void authorizeWhenCustomActorTokenResolverSetThenCalled() { assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); verify(actorTokenResolver).apply(authorizationContext); ArgumentCaptor grantRequestCaptor = ArgumentCaptor .forClass(TokenExchangeGrantRequest.class); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientAuthorizationCodeTokenResponseClientTests.java index f365a93291c..95d6bb188e7 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientAuthorizationCodeTokenResponseClientTests.java @@ -445,6 +445,38 @@ public void getTokenResponseWhenParametersConverterSetThenCalled() throws Except assertThat(formParameters).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration.build(); + OAuth2AuthorizationCodeGrantRequest grantRequest = new OAuth2AuthorizationCodeGrantRequest(clientRegistration, + this.authorizationExchange); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.CODE, "custom-code"); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, "custom-uri"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((authorizationGrantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(grantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + String formParameters = recordedRequest.getBody().readUtf8(); + // @formatter:off + assertThat(formParameters).contains( + param(OAuth2ParameterNames.GRANT_TYPE, "custom"), + param(OAuth2ParameterNames.CODE, "custom-code"), + param(OAuth2ParameterNames.REDIRECT_URI, "custom-uri")); + // @formatter:on + assertThat(formParameters).doesNotContain(OAuth2ParameterNames.CLIENT_ID); + } + @Test public void getTokenResponseWhenParametersConverterAddedThenCalled() throws Exception { // @formatter:off diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientClientCredentialsTokenResponseClientTests.java index c97a02ca1ba..bd9fd031139 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientClientCredentialsTokenResponseClientTests.java @@ -453,6 +453,38 @@ public void getTokenResponseWhenParametersConverterSetThenCalled() throws Except assertThat(formParameters).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration.build(); + OAuth2ClientCredentialsGrantRequest grantRequest = new OAuth2ClientCredentialsGrantRequest(clientRegistration); + Converter> parametersConverter = mock( + Converter.class); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + given(parametersConverter.convert(grantRequest)).willReturn(parameters); + this.tokenResponseClient.setParametersConverter((authorizationGrantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(grantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + String formParameters = recordedRequest.getBody().readUtf8(); + // @formatter:off + assertThat(formParameters).contains( + param(OAuth2ParameterNames.GRANT_TYPE, "custom"), + param(OAuth2ParameterNames.SCOPE, "one two")); + // @formatter:on + assertThat(formParameters).doesNotContain(OAuth2ParameterNames.CLIENT_ID); + } + @Test public void getTokenResponseWhenParametersConverterAddedThenCalled() throws Exception { // @formatter:off diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientJwtBearerTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientJwtBearerTokenResponseClientTests.java index db8c8228887..91d2649942b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientJwtBearerTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientJwtBearerTokenResponseClientTests.java @@ -396,6 +396,38 @@ public void getTokenResponseWhenHeadersConverterSetThenCalled() throws Exception @Test public void getTokenResponseWhenParametersConverterSetThenCalled() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration.build(); + JwtBearerGrantRequest grantRequest = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.ASSERTION, "custom-assertion"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((authorizationGrantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(grantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + String formParameters = recordedRequest.getBody().readUtf8(); + // @formatter:off + assertThat(formParameters).contains( + param(OAuth2ParameterNames.GRANT_TYPE, "custom"), + param(OAuth2ParameterNames.ASSERTION, "custom-assertion"), + param(OAuth2ParameterNames.SCOPE, "one two")); + // @formatter:on + assertThat(formParameters).doesNotContain(OAuth2ParameterNames.CLIENT_ID); + } + + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); // @formatter:off String accessTokenSuccessResponse = "{\n" + " \"access_token\": \"access-token-1234\",\n" diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientRefreshTokenTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientRefreshTokenTokenResponseClientTests.java index bb14848cd2a..3a2e0cf5f71 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientRefreshTokenTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientRefreshTokenTokenResponseClientTests.java @@ -473,6 +473,38 @@ public void getTokenResponseWhenParametersConverterSetThenCalled() throws Except assertThat(formParameters).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration.build(); + OAuth2RefreshTokenGrantRequest grantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration, + this.accessToken, this.refreshToken); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.REFRESH_TOKEN, "custom-token"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((authorizationGrantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(grantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + String formParameters = recordedRequest.getBody().readUtf8(); + // @formatter:off + assertThat(formParameters).contains( + param(OAuth2ParameterNames.GRANT_TYPE, "custom"), + param(OAuth2ParameterNames.REFRESH_TOKEN, "custom-token"), + param(OAuth2ParameterNames.SCOPE, "one two")); + // @formatter:on + assertThat(formParameters).doesNotContain(OAuth2ParameterNames.CLIENT_ID); + } + @Test public void getTokenResponseWhenParametersConverterAddedThenCalled() throws Exception { // @formatter:off diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientTokenExchangeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientTokenExchangeTokenResponseClientTests.java index deb448e248d..1792a57e59f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientTokenExchangeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/RestClientTokenExchangeTokenResponseClientTests.java @@ -569,6 +569,38 @@ public void getTokenResponseWhenParametersConverterSetThenCalled() throws Except assertThat(formParameters).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + ClientRegistration clientRegistration = this.clientRegistration.build(); + TokenExchangeGrantRequest grantRequest = new TokenExchangeGrantRequest(clientRegistration, this.subjectToken, + this.actorToken); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, "custom-token"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((authorizationGrantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(grantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + String formParameters = recordedRequest.getBody().readUtf8(); + // @formatter:off + assertThat(formParameters).contains( + param(OAuth2ParameterNames.GRANT_TYPE, "custom"), + param(OAuth2ParameterNames.SCOPE, "one two"), + param(OAuth2ParameterNames.SUBJECT_TOKEN, "custom-token")); + // @formatter:on + assertThat(formParameters).doesNotContain(OAuth2ParameterNames.CLIENT_ID); + } + @Test public void getTokenResponseWhenParametersConverterAddedThenCalled() throws Exception { // @formatter:off diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/function/client/OAuth2ClientHttpRequestInterceptorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptorTests.java similarity index 95% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/function/client/OAuth2ClientHttpRequestInterceptorTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptorTests.java index d7f98cb105a..f19d81c277d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/function/client/OAuth2ClientHttpRequestInterceptorTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptorTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.security.oauth2.client.web.function.client; +package org.springframework.security.oauth2.client.web.client; import java.util.List; import java.util.Map; @@ -43,7 +43,6 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.oauth2.client.ClientAuthorizationException; import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler; @@ -55,8 +54,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor; -import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -112,15 +109,15 @@ public class OAuth2ClientHttpRequestInterceptorTests { @Mock private OAuth2AuthorizedClientRepository authorizedClientRepository; - @Mock - private SecurityContextHolderStrategy securityContextHolderStrategy; - @Mock private OAuth2AuthorizedClientService authorizedClientService; @Mock private OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver clientRegistrationIdResolver; + @Mock + private OAuth2ClientHttpRequestInterceptor.PrincipalResolver principalResolver; + @Captor private ArgumentCaptor authorizeRequestCaptor; @@ -169,13 +166,6 @@ public void constructorWhenAuthorizedClientManagerIsNullThenThrowsIllegalArgumen .withMessage("authorizedClientManager cannot be null"); } - @Test - public void constructorWhenClientRegistrationIdResolverIsNullThenThrowsIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new OAuth2ClientHttpRequestInterceptor(this.authorizedClientManager, null)) - .withMessage("clientRegistrationIdResolver cannot be null"); - } - @Test public void setAuthorizationFailureHandlerWhenNullThenThrowsIllegalArgumentException() { assertThatIllegalArgumentException() @@ -200,10 +190,16 @@ public void authorizationFailureHandlerWhenAuthorizedClientServiceIsNullThenThro } @Test - public void setSecurityContextHolderStrategyWhenNullThenThrowsIllegalArgumentException() { + public void setClientRegistrationIdResolverWhenNullThenThrowsIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> this.requestInterceptor.setSecurityContextHolderStrategy(null)) - .withMessage("securityContextHolderStrategy cannot be null"); + .isThrownBy(() -> this.requestInterceptor.setClientRegistrationIdResolver(null)) + .withMessage("clientRegistrationIdResolver cannot be null"); + } + + @Test + public void setPrincipalResolverWhenNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.requestInterceptor.setPrincipalResolver(null)) + .withMessage("principalResolver cannot be null"); } @Test @@ -606,9 +602,8 @@ public void interceptWhenUnauthorizedAndAuthorizationFailureHandlerSetWithAuthor } @Test - public void interceptWhenClientRegistrationIdResolverSetThenUsed() { - this.requestInterceptor = new OAuth2ClientHttpRequestInterceptor(this.authorizedClientManager, - this.clientRegistrationIdResolver); + public void interceptWhenCustomClientRegistrationIdResolverSetThenUsed() { + this.requestInterceptor.setClientRegistrationIdResolver(this.clientRegistrationIdResolver); this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler); given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class))) .willReturn(this.authorizedClient); @@ -627,7 +622,7 @@ public void interceptWhenClientRegistrationIdResolverSetThenUsed() { this.server.verify(); verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture()); verify(this.clientRegistrationIdResolver).resolve(any(HttpRequest.class)); - verifyNoMoreInteractions(this.clientRegistrationIdResolver, this.authorizedClientManager); + verifyNoMoreInteractions(this.authorizedClientManager, this.clientRegistrationIdResolver); verifyNoInteractions(this.authorizationFailureHandler); OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue(); assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(clientRegistrationId); @@ -635,8 +630,8 @@ public void interceptWhenClientRegistrationIdResolverSetThenUsed() { } @Test - public void interceptWhenCustomSecurityContextHolderStrategySetThenUsed() { - this.requestInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + public void interceptWhenCustomPrincipalResolverSetThenUsed() { + this.requestInterceptor.setPrincipalResolver(this.principalResolver); given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class))) .willReturn(this.authorizedClient); @@ -644,14 +639,12 @@ public void interceptWhenCustomSecurityContextHolderStrategySetThenUsed() { this.server.expect(requestTo(REQUEST_URI)) .andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken())) .andRespond(withApplicationJson()); - SecurityContext securityContext = new SecurityContextImpl(); - securityContext.setAuthentication(this.principal); - given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext); + given(this.principalResolver.resolve(any(HttpRequest.class))).willReturn(this.principal); performRequest(withClientRegistrationId()); this.server.verify(); verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture()); - verify(this.securityContextHolderStrategy).getContext(); - verifyNoMoreInteractions(this.authorizedClientManager); + verify(this.principalResolver).resolve(any(HttpRequest.class)); + verifyNoMoreInteractions(this.authorizedClientManager, this.principalResolver); OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue(); assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId()); assertThat(authorizeRequest.getPrincipal()).isEqualTo(this.principal); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java index 8522af771c9..ff40a307858 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.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. @@ -440,7 +440,21 @@ private String buildAuthorizationRequestUri() { Map parameters = getParameters(); // Not encoded this.parametersConsumer.accept(parameters); MultiValueMap queryParams = new LinkedMultiValueMap<>(); - parameters.forEach((k, v) -> queryParams.set(encodeQueryParam(k), encodeQueryParam(String.valueOf(v)))); // Encoded + parameters.forEach((k, v) -> { + String key = encodeQueryParam(k); + if (v instanceof Iterable) { + ((Iterable) v).forEach((value) -> queryParams.add(key, encodeQueryParam(String.valueOf(value)))); + } + else if (v != null && v.getClass().isArray()) { + Object[] values = (Object[]) v; + for (Object value : values) { + queryParams.add(key, encodeQueryParam(String.valueOf(value))); + } + } + else { + queryParams.set(key, encodeQueryParam(String.valueOf(v))); + } + }); UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams); return this.authorizationRequestUriFunction.apply(uriBuilder).toString(); } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java index 1a912d58a8e..1c4365560d8 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.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. @@ -19,6 +19,7 @@ import java.net.URI; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -319,4 +320,49 @@ public void buildWhenNonAsciiAdditionalParametersThenProperlyEncoded() { + "item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96"); } + @Test + public void buildWhenAdditionalParametersContainsArrayThenProperlyEncoded() { + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("item1", new String[] { "1", "2" }); + additionalParameters.put("item2", "value2"); + OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() + .additionalParameters(additionalParameters) + .build(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull(); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&" + + "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" + + "item1=1&item1=2&item2=value2"); + } + + @Test + public void buildWhenAdditionalParametersContainsIterableThenProperlyEncoded() { + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("item1", Arrays.asList("1", "2")); + additionalParameters.put("item2", "value2"); + OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() + .additionalParameters(additionalParameters) + .build(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull(); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&" + + "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" + + "item1=1&item1=2&item2=value2"); + } + + @Test + public void buildWhenAdditionalParametersContainsNullThenAuthorizationRequestUriContainsNull() { + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("item1", null); + additionalParameters.put("item2", "value2"); + OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() + .additionalParameters(additionalParameters) + .build(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull(); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&" + + "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" + + "item1=null&item2=value2"); + } + } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java index 9f231d92f78..35dc7cdedf2 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -49,11 +49,12 @@ private ReactiveJwtDecoders() { * @return a {@link ReactiveJwtDecoder} that was initialized by the OpenID Provider * Configuration. */ - public static ReactiveJwtDecoder fromOidcIssuerLocation(String oidcIssuerLocation) { + @SuppressWarnings("unchecked") + public static T fromOidcIssuerLocation(String oidcIssuerLocation) { Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty"); Map configuration = JwtDecoderProviderConfigurationUtils .getConfigurationForOidcIssuerLocation(oidcIssuerLocation); - return withProviderConfiguration(configuration, oidcIssuerLocation); + return (T) withProviderConfiguration(configuration, oidcIssuerLocation); } /** @@ -85,11 +86,12 @@ public static ReactiveJwtDecoder fromOidcIssuerLocation(String oidcIssuerLocatio * @return a {@link ReactiveJwtDecoder} that was initialized by one of the described * endpoints */ - public static ReactiveJwtDecoder fromIssuerLocation(String issuer) { + @SuppressWarnings("unchecked") + public static T fromIssuerLocation(String issuer) { Assert.hasText(issuer, "issuer cannot be empty"); Map configuration = JwtDecoderProviderConfigurationUtils .getConfigurationForIssuerLocation(issuer); - return withProviderConfiguration(configuration, issuer); + return (T) withProviderConfiguration(configuration, issuer); } /** diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverter.java new file mode 100644 index 00000000000..13d71814975 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverter.java @@ -0,0 +1,118 @@ +/* + * 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.oauth2.server.resource.authentication; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.log.LogMessage; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +/** + * Uses an expression for extracting the token claim value to use for mapping + * {@link GrantedAuthority authorities}. + * + * Note this can be used in combination with a + * {@link DelegatingJwtGrantedAuthoritiesConverter}. + * + * @author Thomas Darimont + * @since 6.4 + */ +public final class ExpressionJwtGrantedAuthoritiesConverter implements Converter> { + + private final Log logger = LogFactory.getLog(getClass()); + + private String authorityPrefix = "SCOPE_"; + + private final Expression authoritiesClaimExpression; + + /** + * Constructs a {@link ExpressionJwtGrantedAuthoritiesConverter} using the provided + * {@code authoritiesClaimExpression}. + * @param authoritiesClaimExpression The token claim SpEL Expression to map + * authorities from. + */ + public ExpressionJwtGrantedAuthoritiesConverter(Expression authoritiesClaimExpression) { + Assert.notNull(authoritiesClaimExpression, "authoritiesClaimExpression must not be null"); + this.authoritiesClaimExpression = authoritiesClaimExpression; + } + + /** + * Sets the prefix to use for {@link GrantedAuthority authorities} mapped by this + * converter. Defaults to {@code "SCOPE_"}. + * @param authorityPrefix The authority prefix + */ + public void setAuthorityPrefix(String authorityPrefix) { + Assert.notNull(authorityPrefix, "authorityPrefix cannot be null"); + this.authorityPrefix = authorityPrefix; + } + + /** + * Extract {@link GrantedAuthority}s from the given {@link Jwt}. + * @param jwt The {@link Jwt} token + * @return The {@link GrantedAuthority authorities} read from the token scopes + */ + @Override + public Collection convert(Jwt jwt) { + Collection grantedAuthorities = new ArrayList<>(); + for (String authority : getAuthorities(jwt)) { + grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority)); + } + return grantedAuthorities; + } + + private Collection getAuthorities(Jwt jwt) { + Object authorities; + try { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Looking for authorities with expression. expression=%s", + this.authoritiesClaimExpression.getExpressionString())); + } + authorities = this.authoritiesClaimExpression.getValue(jwt.getClaims(), Collection.class); + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Found authorities with expression. authorities=%s", authorities)); + } + } + catch (ExpressionException ee) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Failed to evaluate expression. error=%s", ee.getMessage())); + } + authorities = Collections.emptyList(); + } + + if (authorities != null) { + return castAuthoritiesToCollection(authorities); + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + private Collection castAuthoritiesToCollection(Object authorities) { + return (Collection) authorities; + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverterTests.java new file mode 100644 index 00000000000..9c55e7e4656 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ExpressionJwtGrantedAuthoritiesConverterTests.java @@ -0,0 +1,101 @@ +/* + * 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.oauth2.server.resource.authentication; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.TestJwts; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExpressionJwtGrantedAuthoritiesConverter} + * + * @author Thomas Darimont + * @since 6.4 + */ +public class ExpressionJwtGrantedAuthoritiesConverterTests { + + @Test + public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthorities() { + // @formatter:off + Jwt jwt = TestJwts.jwt() + .claim("nested", Collections.singletonMap("roles", Arrays.asList("role1", "role2"))) + .build(); + // @formatter:on + SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]"); + ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter( + expression); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_role1"), + new SimpleGrantedAuthority("SCOPE_role2")); + } + + @Test + public void convertToEmptyListWhenTokenClaimExpressionYieldsNull() { + // @formatter:off + Jwt jwt = TestJwts.jwt() + .claim("nested", Collections.singletonMap("roles", null)) + .build(); + // @formatter:on + SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]"); + ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter( + expression); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + assertThat(authorities).isEmpty(); + } + + @Test + public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthoritiesWithPrefix() { + // @formatter:off + Jwt jwt = TestJwts.jwt() + .claim("nested", Collections.singletonMap("roles", Arrays.asList("role1", "role2"))) + .build(); + // @formatter:on + SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]"); + ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter( + expression); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix("CUSTOM_"); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + assertThat(authorities).containsExactly(new SimpleGrantedAuthority("CUSTOM_role1"), + new SimpleGrantedAuthority("CUSTOM_role2")); + } + + @Test + public void convertWhenTokenHasCustomInvalidClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToEmptyAuthorities() { + // @formatter:off + Jwt jwt = TestJwts.jwt() + .claim("other", Collections.singletonMap("roles", Arrays.asList("role1", "role2"))) + .build(); + // @formatter:on + SpelExpression expression = new SpelExpressionParser().parseRaw("[nested][roles]"); + ExpressionJwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new ExpressionJwtGrantedAuthoritiesConverter( + expression); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + assertThat(authorities).isEmpty(); + } + +} diff --git a/settings.gradle b/settings.gradle index a9a58ed3b49..6ebece46a87 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ pluginManagement { } plugins { - id "io.spring.develocity.conventions" version "0.0.21" + id "io.spring.develocity.conventions" version "0.0.22" } dependencyResolutionManagement { diff --git a/web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java index 8bb88cf17b1..a6a3133a849 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java @@ -45,14 +45,16 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter { private final OneTimeTokenService oneTimeTokenService; - private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate"); + private final GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler; - private GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler = new RedirectGeneratedOneTimeTokenHandler( - "/login/ott"); + private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate"); - public GenerateOneTimeTokenFilter(OneTimeTokenService oneTimeTokenService) { + public GenerateOneTimeTokenFilter(OneTimeTokenService oneTimeTokenService, + GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler) { Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null"); + Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null"); this.oneTimeTokenService = oneTimeTokenService; + this.generatedOneTimeTokenHandler = generatedOneTimeTokenHandler; } @Override @@ -81,14 +83,4 @@ public void setRequestMatcher(RequestMatcher requestMatcher) { this.requestMatcher = requestMatcher; } - /** - * Specifies {@link GeneratedOneTimeTokenHandler} to be used to handle generated - * one-time tokens - * @param generatedOneTimeTokenHandler - */ - public void setGeneratedOneTimeTokenHandler(GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler) { - Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null"); - this.generatedOneTimeTokenHandler = generatedOneTimeTokenHandler; - } - } diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java index 2e7a9d09ba1..2c1f8aac109 100644 --- a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java +++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java @@ -57,7 +57,7 @@ public class WebSessionServerRequestCache implements ServerRequestCache { private String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR; - private ServerWebExchangeMatcher saveRequestMatcher = createDefaultRequestMacher(); + private ServerWebExchangeMatcher saveRequestMatcher = createDefaultRequestMatcher(); private String matchingRequestParameterName; @@ -156,7 +156,7 @@ private URI createRedirectUri(String uri) { // @formatter:on } - private static ServerWebExchangeMatcher createDefaultRequestMacher() { + private static ServerWebExchangeMatcher createDefaultRequestMatcher() { ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**"); ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher( ServerWebExchangeMatchers.pathMatchers("/favicon.*"));