From a2becc1179dd464d8ae1c91a63b0bfd366bc211f Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Thu, 17 Jul 2025 15:08:33 +0300 Subject: [PATCH] split multiple auto-config classes into separate files Signed-off-by: Andrey Litvitski --- .../ExceptionHandlerAutoConfiguration.java | 34 +++ ...veSecurityConfigurerAutoConfiguration.java | 44 +++ .../GrpcSecurityAutoConfiguration.java | 68 ----- ...etSecurityConfigurerAutoConfiguration.java | 49 +++ ...OAuth2ResourceServerAutoConfiguration.java | 278 +----------------- .../OAuth2ResourceServerConfiguration.java | 43 +++ .../OAuth2ResourceServerJwtConfiguration.java | 230 +++++++++++++++ ...esourceServerOpaqueTokenConfiguration.java | 66 +++++ 8 files changed, 470 insertions(+), 342 deletions(-) create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/ExceptionHandlerAutoConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcNativeSecurityConfigurerAutoConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcServletSecurityConfigurerAutoConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerJwtConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerOpaqueTokenConfiguration.java diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/ExceptionHandlerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/ExceptionHandlerAutoConfiguration.java new file mode 100644 index 00000000..384f1cdc --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/ExceptionHandlerAutoConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; + +@Configuration(proxyBeanMethods = false) +@Import(AuthenticationConfiguration.class) +public class ExceptionHandlerAutoConfiguration { + + @Bean + public GrpcExceptionHandler accessExceptionHandler() { + return new SecurityGrpcExceptionHandler(); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcNativeSecurityConfigurerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcNativeSecurityConfigurerAutoConfiguration.java new file mode 100644 index 00000000..500a4b7f --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcNativeSecurityConfigurerAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; + +@ConditionalOnBean(ObjectPostProcessor.class) +@Configuration(proxyBeanMethods = false) +@Conditional(GrpcServerFactoryAutoConfiguration.OnNativeGrpcServerCondition.class) +public class GrpcNativeSecurityConfigurerAutoConfiguration { + + @Bean + public GrpcSecurity grpcSecurity(ObjectPostProcessor objectPostProcessor, + AuthenticationConfiguration authenticationConfiguration, ApplicationContext context) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration + .authenticationManagerBuilder(objectPostProcessor, context); + authenticationManagerBuilder + .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); + return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java index a8c853a2..cd31e89a 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java @@ -13,35 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -//CHECKSTYLE:OFF package org.springframework.grpc.autoconfigure.server.security; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.grpc.autoconfigure.server.ConditionalOnGrpcServerEnabled; -import org.springframework.grpc.autoconfigure.server.GrpcServerExecutorProvider; -import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; import org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration; -import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.exception.GrpcExceptionHandler; -import org.springframework.grpc.server.security.GrpcSecurity; -import org.springframework.grpc.server.security.SecurityContextServerInterceptor; -import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; -import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; 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.web.SecurityFilterChain; - -import io.grpc.internal.GrpcUtil; @ConditionalOnClass(ObjectPostProcessor.class) @ConditionalOnGrpcServerEnabled @@ -51,51 +31,3 @@ public class GrpcSecurityAutoConfiguration { } - -@Configuration(proxyBeanMethods = false) -@Import(AuthenticationConfiguration.class) -class ExceptionHandlerAutoConfiguration { - - @Bean - public GrpcExceptionHandler accessExceptionHandler() { - return new SecurityGrpcExceptionHandler(); - } - -} - -@ConditionalOnBean(ObjectPostProcessor.class) -@Configuration(proxyBeanMethods = false) -@Conditional(GrpcServerFactoryAutoConfiguration.OnNativeGrpcServerCondition.class) -class GrpcNativeSecurityConfigurerAutoConfiguration { - - @Bean - public GrpcSecurity grpcSecurity(ObjectPostProcessor objectPostProcessor, - AuthenticationConfiguration authenticationConfiguration, ApplicationContext context) throws Exception { - AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration - .authenticationManagerBuilder(objectPostProcessor, context); - authenticationManagerBuilder - .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); - return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); - } - -} - -@ConditionalOnBean(SecurityFilterChain.class) -@Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class) -@Configuration(proxyBeanMethods = false) -class GrpcServletSecurityConfigurerAutoConfiguration { - - @Bean - @GlobalServerInterceptor - public SecurityContextServerInterceptor securityContextInterceptor() { - return new SecurityContextServerInterceptor(); - } - - @Bean - @ConditionalOnMissingBean(GrpcServerExecutorProvider.class) - public GrpcServerExecutorProvider grpcServerExecutorProvider() { - return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create()); - } - -} -// CHECKSTYLE:ON diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcServletSecurityConfigurerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcServletSecurityConfigurerAutoConfiguration.java new file mode 100644 index 00000000..320e90b6 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcServletSecurityConfigurerAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.autoconfigure.server.GrpcServerExecutorProvider; +import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.security.SecurityContextServerInterceptor; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import org.springframework.security.web.SecurityFilterChain; + +import io.grpc.internal.GrpcUtil; + +@ConditionalOnBean(SecurityFilterChain.class) +@Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class) +@Configuration(proxyBeanMethods = false) +public class GrpcServletSecurityConfigurerAutoConfiguration { + + @Bean + @GlobalServerInterceptor + public SecurityContextServerInterceptor securityContextInterceptor() { + return new SecurityContextServerInterceptor(); + } + + @Bean + @ConditionalOnMissingBean(GrpcServerExecutorProvider.class) + public GrpcServerExecutorProvider grpcServerExecutorProvider() { + return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create()); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerAutoConfiguration.java index d3b63a1c..35b89a5a 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerAutoConfiguration.java @@ -13,67 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -//CHECKSTYLE:OFF package org.springframework.grpc.autoconfigure.server.security; -import static org.springframework.security.config.Customizer.withDefaults; - -import java.security.KeyFactory; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition; -import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.JwkSetUriJwtDecoderBuilderCustomizer; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration.GrpcServletConfiguration; -import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; -import org.springframework.grpc.server.security.GrpcSecurity; import org.springframework.security.config.ObjectPostProcessor; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.jwt.JwtClaimValidator; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtValidators; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; -import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; -import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; -import org.springframework.util.CollectionUtils; import io.grpc.BindableService; -// All copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/43978), except the -// 2 @Beans of type AuthenticationProcessInterceptor +// Copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/43978) @AutoConfiguration(before = { UserDetailsServiceAutoConfiguration.class }, after = { SecurityAutoConfiguration.class, GrpcSecurityAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration.class }) @@ -81,236 +39,8 @@ @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ObjectPostProcessor.class }) @ConditionalOnMissingBean(GrpcServletConfiguration.class) @ConditionalOnBean({ BindableService.class, GrpcSecurityAutoConfiguration.class }) -@Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class, - Oauth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) -class OAuth2ResourceServerAutoConfiguration { - -} - -@Configuration(proxyBeanMethods = false) -class Oauth2ResourceServerConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(JwtDecoder.class) - @Import({ OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class, - OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, - OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) - static class JwtConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, - OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class }) - static class OpaqueTokenConfiguration { - - } - -} - -@Configuration(proxyBeanMethods = false) -class OAuth2ResourceServerOpaqueTokenConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(OpaqueTokenIntrospector.class) - static class OpaqueTokenIntrospectionClientConfiguration { - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") - SpringOpaqueTokenIntrospector blockingOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { - OAuth2ResourceServerProperties.Opaquetoken opaqueToken = properties.getOpaquetoken(); - return new SpringOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(), - opaqueToken.getClientSecret()); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) - static class OAuth2SecurityFilterChainConfiguration { - - @Bean - @ConditionalOnBean(OpaqueTokenIntrospector.class) - @GlobalServerInterceptor - AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity http) - throws Exception { - http.authorizeRequests((requests) -> requests.allRequests().authenticated()); - http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); - return http.build(); - } - - } - -} - -@Configuration(proxyBeanMethods = false) -class OAuth2ResourceServerJwtConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(JwtDecoder.class) - static class JwtDecoderConfiguration { - - private final OAuth2ResourceServerProperties.Jwt properties; - - private final List> additionalValidators; - - JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, - ObjectProvider> additionalValidators) { - this.properties = properties.getJwt(); - this.additionalValidators = additionalValidators.orderedStream().toList(); - } - - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - JwtDecoder blockingJwtDecoderByJwkKeySetUri(ObjectProvider customizers) { - JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithms(this::jwsAlgorithms); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusJwtDecoder nimbusJwtDecoder = builder.build(); - String issuerUri = this.properties.getIssuerUri(); - OAuth2TokenValidator defaultValidator = (issuerUri != null) - ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault(); - nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); - return nimbusJwtDecoder; - } - - private void jwsAlgorithms(Set signatureAlgorithms) { - for (String algorithm : this.properties.getJwsAlgorithms()) { - signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); - } - } - - private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) { - List audiences = this.properties.getAudiences(); - if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { - return defaultValidator; - } - List> validators = new ArrayList<>(); - validators.add(defaultValidator); - if (!CollectionUtils.isEmpty(audiences)) { - validators.add(audValidator(audiences)); - } - validators.addAll(this.additionalValidators); - return new DelegatingOAuth2TokenValidator<>(validators); - } - - private JwtClaimValidator> audValidator(List audiences) { - return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences)); - } - - private boolean nullSafeDisjoint(List c1, List c2) { - return c1 != null && !Collections.disjoint(c1, c2); - } - - @Bean - @Conditional(KeyValueCondition.class) - JwtDecoder blockingJwtDecoderByPublicKeyValue() throws Exception { - RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) - .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) - .build(); - jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault())); - return jwtDecoder; - } - - private byte[] getKeySpec(String keyValue) { - keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); - return Base64.getMimeDecoder().decode(keyValue); - } - - private String exactlyOneAlgorithm() { - List algorithms = this.properties.getJwsAlgorithms(); - int count = (algorithms != null) ? algorithms.size() : 0; - if (count != 1) { - throw new IllegalStateException( - "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count - + " were configured"); - } - return algorithms.get(0); - } - - @Bean - @Conditional(IssuerUriCondition.class) - SupplierJwtDecoder blockingJwtDecoderByIssuerUri( - ObjectProvider customizers) { - return new SupplierJwtDecoder(() -> { - String issuerUri = this.properties.getIssuerUri(); - JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - NimbusJwtDecoder jwtDecoder = builder.build(); - jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri))); - return jwtDecoder; - }); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) - static class OAuth2SecurityFilterChainConfiguration { - - @Bean - @ConditionalOnBean(JwtDecoder.class) - @GlobalServerInterceptor - AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity http) throws Exception { - http.authorizeRequests((requests) -> requests.allRequests().authenticated()); - http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); - return http.build(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(JwtAuthenticationConverter.class) - @Conditional(JwtConverterPropertiesCondition.class) - static class JwtConverterConfiguration { - - private final OAuth2ResourceServerProperties.Jwt properties; - - JwtConverterConfiguration(OAuth2ResourceServerProperties properties) { - this.properties = properties.getJwt(); - } - - @Bean - JwtAuthenticationConverter getJwtAuthenticationConverter() { - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); - map.from(this.properties.getAuthoritiesClaimDelimiter()) - .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); - map.from(this.properties.getAuthoritiesClaimName()) - .to(grantedAuthoritiesConverter::setAuthoritiesClaimName); - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); - return jwtAuthenticationConverter; - } - - } - - private static class JwtConverterPropertiesCondition extends AnyNestedCondition { - - JwtConverterPropertiesCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix") - static class OnAuthorityPrefix { - - } - - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name") - static class OnPrincipalClaimName { - - } - - @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name") - static class OnAuthoritiesClaimName { - - } - - } +@Import({ OAuth2ResourceServerConfiguration.JwtConfiguration.class, + OAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) +public class OAuth2ResourceServerAutoConfiguration { } -// CHECKSTYLE:ON diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerConfiguration.java new file mode 100644 index 00000000..82daa068 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +// Copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/43978) +@Configuration(proxyBeanMethods = false) +public class OAuth2ResourceServerConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JwtDecoder.class) + @Import({ OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class, + OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, + OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class OpaqueTokenConfiguration { + + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerJwtConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerJwtConfiguration.java new file mode 100644 index 00000000..7fc267b5 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerJwtConfiguration.java @@ -0,0 +1,230 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition; +import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.JwkSetUriJwtDecoderBuilderCustomizer; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.Customizer; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.CollectionUtils; + +// Copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/43978), except the +// @Bean of type AuthenticationProcessInterceptor +@Configuration(proxyBeanMethods = false) +public class OAuth2ResourceServerJwtConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtDecoder.class) + static class JwtDecoderConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + private final List> additionalValidators; + + JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, + ObjectProvider> additionalValidators) { + this.properties = properties.getJwt(); + this.additionalValidators = additionalValidators.orderedStream().toList(); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + JwtDecoder blockingJwtDecoderByJwkKeySetUri(ObjectProvider customizers) { + NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder + .withJwkSetUri(this.properties.getJwkSetUri()) + .jwsAlgorithms(this::jwsAlgorithms); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder nimbusJwtDecoder = builder.build(); + String issuerUri = this.properties.getIssuerUri(); + OAuth2TokenValidator defaultValidator = (issuerUri != null) + ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault(); + nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); + return nimbusJwtDecoder; + } + + private void jwsAlgorithms(Set signatureAlgorithms) { + for (String algorithm : this.properties.getJwsAlgorithms()) { + signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); + } + } + + private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) { + List audiences = this.properties.getAudiences(); + if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { + return defaultValidator; + } + List> validators = new ArrayList<>(); + validators.add(defaultValidator); + if (!CollectionUtils.isEmpty(audiences)) { + validators.add(audValidator(audiences)); + } + validators.addAll(this.additionalValidators); + return new DelegatingOAuth2TokenValidator<>(validators); + } + + private JwtClaimValidator> audValidator(List audiences) { + return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences)); + } + + private boolean nullSafeDisjoint(List c1, List c2) { + return c1 != null && !Collections.disjoint(c1, c2); + } + + @Bean + @Conditional(KeyValueCondition.class) + JwtDecoder blockingJwtDecoderByPublicKeyValue() throws Exception { + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) + .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) + .build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault())); + return jwtDecoder; + } + + private byte[] getKeySpec(String keyValue) { + keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); + return Base64.getMimeDecoder().decode(keyValue); + } + + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + int count = (algorithms != null) ? algorithms.size() : 0; + if (count != 1) { + throw new IllegalStateException( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count + + " were configured"); + } + return algorithms.get(0); + } + + @Bean + @Conditional(IssuerUriCondition.class) + SupplierJwtDecoder blockingJwtDecoderByIssuerUri( + ObjectProvider customizers) { + return new SupplierJwtDecoder(() -> { + String issuerUri = this.properties.getIssuerUri(); + NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder jwtDecoder = builder.build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri))); + return jwtDecoder; + }); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) + static class OAuth2SecurityFilterChainConfiguration { + + @Bean + @ConditionalOnBean(JwtDecoder.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.allRequests().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults())); + return http.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtAuthenticationConverter.class) + @Conditional(JwtConverterPropertiesCondition.class) + static class JwtConverterConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtConverterConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + JwtAuthenticationConverter getJwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); + map.from(this.properties.getAuthoritiesClaimDelimiter()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); + map.from(this.properties.getAuthoritiesClaimName()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } + + } + + private static class JwtConverterPropertiesCondition extends AnyNestedCondition { + + JwtConverterPropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerOpaqueTokenConfiguration.java new file mode 100644 index 00000000..15839aa9 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/OAuth2ResourceServerOpaqueTokenConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-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.grpc.autoconfigure.server.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.Customizer; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; + +// Copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/43978), except the +// @Bean of type AuthenticationProcessInterceptor +@Configuration(proxyBeanMethods = false) +public class OAuth2ResourceServerOpaqueTokenConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(OpaqueTokenIntrospector.class) + static class OpaqueTokenIntrospectionClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") + SpringOpaqueTokenIntrospector blockingOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.Opaquetoken opaqueToken = properties.getOpaquetoken(); + return new SpringOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(), + opaqueToken.getClientSecret()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) + static class OAuth2SecurityFilterChainConfiguration { + + @Bean + @ConditionalOnBean(OpaqueTokenIntrospector.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity http) + throws Exception { + http.authorizeRequests((requests) -> requests.allRequests().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(Customizer.withDefaults())); + return http.build(); + } + + } + +}