diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle
index 581e145e640..28f6200c30e 100644
--- a/module/spring-boot-grpc-server/build.gradle
+++ b/module/spring-boot-grpc-server/build.gradle
@@ -31,6 +31,8 @@ dependencies {
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-health"))
optional(project(":module:spring-boot-micrometer-observation"))
+ optional(project(":module:spring-boot-security"))
+ optional(project(":module:spring-boot-security-oauth2-resource-server"))
optional("com.fasterxml.jackson.core:jackson-annotations")
optional("io.projectreactor:reactor-core")
optional("io.grpc:grpc-servlet-jakarta")
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java
new file mode 100644
index 00000000000..fa407365734
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.boot.grpc.server.GrpcServletRegistration;
+import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest;
+import org.springframework.context.ApplicationContext;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC
+ * requests.
+ *
+ * This configurer checks the application context to determine if CSRF protection should
+ * be disabled for gRPC requests based on the property
+ * {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is
+ * disabled unless explicitly enabled in the application properties.
+ *
+ *
+ * @author Dave Syer
+ * @see AbstractHttpConfigurer
+ * @see HttpSecurity
+ */
+class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer {
+
+ @Override
+ public void init(HttpSecurity http) {
+ ApplicationContext context = http.getSharedObject(ApplicationContext.class);
+ if (context != null && isCsrfConfigurerPresent(http) && hasBean(context, GrpcServiceDiscoverer.class)
+ && hasBean(context, GrpcServletRegistration.class) && isCsrfEnabled(context)) {
+ http.csrf(this::disable);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean isCsrfConfigurerPresent(HttpSecurity http) {
+ return http.getConfigurer(CsrfConfigurer.class) != null;
+ }
+
+ private boolean hasBean(ApplicationContext context, Class> type) {
+ return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type));
+ }
+
+ private boolean isCsrfEnabled(ApplicationContext context) {
+ return context.getEnvironment().getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, true);
+ }
+
+ private void disable(CsrfConfigurer csrf) {
+ csrf.requireCsrfProtectionMatcher(GrpcCsrfRequestMatcher.INSTANCE);
+ }
+
+ static class GrpcCsrfRequestMatcher implements RequestMatcher {
+
+ static GrpcCsrfRequestMatcher INSTANCE = new GrpcCsrfRequestMatcher();
+
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ return CsrfFilter.DEFAULT_CSRF_MATCHER.matches(request) && !GrpcRequest.toAnyService().matches(request);
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java
new file mode 100644
index 00000000000..26350670885
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import io.grpc.BindableService;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+import org.springframework.grpc.server.GrpcServerFactory;
+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.jwt.JwtDecoder;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server.
+ *
+ * @author Dave Syer
+ * @author Andrey Litvitski
+ * @author Phillip Webb
+ * @since 4.1.0
+ */
+@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration",
+ afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration",
+ "org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration" },
+ after = { GrpcServerSecurityAutoConfiguration.class, GrpcServerAutoConfiguration.class })
+@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
+@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
+@ConditionalOnMissingBean(AuthenticationProcessInterceptor.class)
+@ConditionalOnBean(GrpcSecurity.class)
+public final class GrpcServerOAuth2ResourceServerAutoConfiguration {
+
+ @Bean
+ @ConditionalOnBean(OpaqueTokenIntrospector.class)
+ @GlobalServerInterceptor
+ AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity)
+ throws Exception {
+ grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
+ grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
+ return grpcSecurity.build();
+ }
+
+ @Bean
+ @ConditionalOnBean(JwtDecoder.class)
+ @GlobalServerInterceptor
+ AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity grpcSecurity) throws Exception {
+ grpcSecurity.authorizeRequests((requests) -> requests.allRequests().authenticated());
+ grpcSecurity.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
+ return grpcSecurity.build();
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java
new file mode 100644
index 00000000000..07ef0e7afc0
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfiguration.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import io.grpc.BindableService;
+import io.grpc.internal.GrpcUtil;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.grpc.server.GrpcServletRegistration;
+import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
+import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider;
+import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.ExceptionHandlerConfiguration;
+import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration;
+import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+import org.springframework.grpc.server.GrpcServerFactory;
+import org.springframework.grpc.server.security.CoroutineSecurityContextInterceptor;
+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.config.annotation.authentication.configuration.EnableGlobalAuthentication;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security.
+ *
+ * @author Dave Syer
+ * @author Chris Bono
+ * @author Andrey Litvitski
+ * @author Phillip Webb
+ * @since 4.1.0
+ */
+@AutoConfiguration(after = GrpcServerAutoConfiguration.class,
+ afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration")
+@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
+@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, ObjectPostProcessor.class })
+@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class,
+ GrpcServletSecurityConfigurerConfiguration.class })
+public final class GrpcServerSecurityAutoConfiguration {
+
+ @Configuration(proxyBeanMethods = false)
+ @Import(AuthenticationConfiguration.class)
+ static class ExceptionHandlerConfiguration {
+
+ @Bean
+ SecurityGrpcExceptionHandler accessExceptionHandler() {
+ return new SecurityGrpcExceptionHandler();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnBean(GrpcServerFactory.class)
+ @EnableGlobalAuthentication
+ static class GrpcNativeSecurityConfigurerConfiguration {
+
+ @Bean
+ GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor objectPostProcessor,
+ AuthenticationConfiguration authenticationConfiguration) {
+ AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration
+ .authenticationManagerBuilder(objectPostProcessor, context);
+ authenticationManagerBuilder
+ .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
+ return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
+ }
+
+ }
+
+ @ConditionalOnBean({ GrpcServletRegistration.class, SecurityFilterChain.class })
+ @Configuration(proxyBeanMethods = false)
+ static class GrpcServletSecurityConfigurerConfiguration {
+
+ @Bean
+ @GlobalServerInterceptor
+ SecurityContextServerInterceptor securityContextInterceptor() {
+ return new SecurityContextServerInterceptor();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ GrpcServerExecutorProvider grpcServerExecutorProvider() {
+ return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create());
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(name = "io.grpc.kotlin.CoroutineContextServerInterceptor")
+ static class GrpcClientCoroutineStubConfiguration {
+
+ @Bean
+ @GlobalServerInterceptor
+ @ConditionalOnMissingBean
+ CoroutineSecurityContextInterceptor coroutineSecurityContextInterceptor() {
+ return new CoroutineSecurityContextInterceptor();
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java
new file mode 100644
index 00000000000..264c92d6e9a
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+/**
+ * Auto-configuration for gRPC server security.
+ */
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure.security;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java
new file mode 100644
index 00000000000..c298429d767
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security.web.reactive;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.Nullable;
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher;
+import org.springframework.boot.security.web.reactive.ApplicationContextServerWebExchangeMatcher;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Factory that can be used to create a {@link ServerWebExchangeMatcher} to match against
+ * gRPC service locations.
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ * @since 4.1.0
+ */
+public final class GrpcRequest {
+
+ private GrpcRequest() {
+ }
+
+ /**
+ * Returns a matcher that includes all gRPC services. The
+ * {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be
+ * used to remove specific services by name if required. For example:
+ *
+ *
+ * GrpcReactiveRequest.toAnyService().excluding("my-service")
+ *
+ * @return the configured {@link ServerWebExchangeMatcher}
+ */
+ public static GrpcReactiveRequestMatcher toAnyService() {
+ return new GrpcReactiveRequestMatcher(Collections.emptySet());
+ }
+
+ /**
+ * The matcher used to match against service locations.
+ */
+ public static final class GrpcReactiveRequestMatcher
+ extends ApplicationContextServerWebExchangeMatcher {
+
+ private static final ServerWebExchangeMatcher EMPTY_MATCHER = (exchange) -> MatchResult.notMatch();
+
+ private final Set excludes;
+
+ private volatile @Nullable ServerWebExchangeMatcher delegate;
+
+ private GrpcReactiveRequestMatcher(Set excludes) {
+ super(GrpcServiceDiscoverer.class);
+ this.excludes = excludes;
+ }
+
+ /**
+ * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
+ * the specified services.
+ * @param services additional services to exclude
+ * @return a new {@link GrpcServletRequestMatcher}
+ */
+ public GrpcReactiveRequestMatcher excluding(String... services) {
+ return excluding(Set.of(services));
+ }
+
+ /**
+ * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
+ * the specified services.
+ * @param services additional service names to exclude
+ * @return a new {@link GrpcServletRequestMatcher}
+ */
+ public GrpcReactiveRequestMatcher excluding(Collection services) {
+ Assert.notNull(services, "'services' must not be null");
+ Set excludes = new LinkedHashSet<>(this.excludes);
+ excludes.addAll(services);
+ return new GrpcReactiveRequestMatcher(excludes);
+ }
+
+ @Override
+ protected void initialized(Supplier context) {
+ this.delegate = createDelegate(context.get());
+
+ }
+
+ private ServerWebExchangeMatcher createDelegate(GrpcServiceDiscoverer serviceDiscoverer) {
+ List delegateMatchers = getDelegateMatchers(serviceDiscoverer);
+ return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers)
+ : EMPTY_MATCHER;
+ }
+
+ private List getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
+ return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
+ }
+
+ private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
+ return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
+ }
+
+ private boolean isExcluded(String service) {
+ return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
+ }
+
+ private String getPath(String service) {
+ return "/" + service + "/**";
+ }
+
+ private ServerWebExchangeMatcher getDelegateMatcher(String path) {
+ Assert.hasText(path, "'path' must not be empty");
+ return new PathPatternParserServerWebExchangeMatcher(path);
+ }
+
+ @Override
+ protected Mono matches(ServerWebExchange exchange, Supplier context) {
+ Assert.state(this.delegate != null, "'delegate' must not be null");
+ return this.delegate.matches(exchange);
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java
new file mode 100644
index 00000000000..51f413b9a25
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+/**
+ * Auto-configuration for gRPC web security when using a servlet stack.
+ */
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure.security.web.reactive;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java
new file mode 100644
index 00000000000..f2eb644214f
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security.web.servlet;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.boot.security.web.servlet.ApplicationContextRequestMatcher;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Factory that can be used to create a {@link RequestMatcher} to match against gRPC
+ * service locations.
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ * @since 4.1.0
+ */
+public final class GrpcRequest {
+
+ private static final RequestMatcher EMPTY_MATCHER = (request) -> false;
+
+ private static final GrpcServletRequestMatcher TO_ANY_SERVICE = new GrpcServletRequestMatcher(
+ Collections.emptySet());
+
+ private GrpcRequest() {
+ }
+
+ /**
+ * Returns a matcher that includes all gRPC services. The
+ * {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used
+ * to remove specific services by name if required. For example:
+ *
+ *
+ * GrpcServletRequest.toAnyService().excluding("my-service")
+ *
+ * @return the configured {@link RequestMatcher}
+ */
+ public static GrpcServletRequestMatcher toAnyService() {
+ return TO_ANY_SERVICE;
+ }
+
+ /**
+ * The matcher used to match against service locations.
+ */
+ public static final class GrpcServletRequestMatcher
+ extends ApplicationContextRequestMatcher {
+
+ private final Set excludes;
+
+ private volatile @Nullable RequestMatcher delegate;
+
+ private GrpcServletRequestMatcher(Set exclusions) {
+ super(GrpcServiceDiscoverer.class);
+ this.excludes = exclusions;
+ }
+
+ /**
+ * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
+ * the specified services.
+ * @param services additional services to exclude
+ * @return a new {@link GrpcServletRequestMatcher}
+ */
+ public GrpcServletRequestMatcher excluding(String... services) {
+ return excluding(Set.of(services));
+ }
+
+ /**
+ * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding
+ * the specified services.
+ * @param services additional service names to exclude
+ * @return a new {@link GrpcServletRequestMatcher}
+ */
+ public GrpcServletRequestMatcher excluding(Collection services) {
+ Assert.notNull(services, "'services' must not be null");
+ Set excludes = new LinkedHashSet<>(this.excludes);
+ excludes.addAll(services);
+ return new GrpcServletRequestMatcher(excludes);
+ }
+
+ @Override
+ protected void initialized(Supplier context) {
+ this.delegate = createDelegate(context.get());
+ }
+
+ private @Nullable RequestMatcher createDelegate(GrpcServiceDiscoverer grpcServiceDiscoverer) {
+ List delegateMatchers = getDelegateMatchers(grpcServiceDiscoverer);
+ return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers)
+ : EMPTY_MATCHER;
+ }
+
+ private List getDelegateMatchers(GrpcServiceDiscoverer serviceDiscoverer) {
+ return getPatterns(serviceDiscoverer).map(this::getDelegateMatcher).toList();
+ }
+
+ private Stream getPatterns(GrpcServiceDiscoverer serviceDiscoverer) {
+ return serviceDiscoverer.listServiceNames().stream().filter(this::isExcluded).map(this::getPath);
+ }
+
+ private boolean isExcluded(String service) {
+ return !this.excludes.stream().anyMatch((candidate) -> candidate.equals(service));
+ }
+
+ private String getPath(String service) {
+ return "/" + service + "/**";
+ }
+
+ private RequestMatcher getDelegateMatcher(String path) {
+ Assert.hasText(path, "'path' must not be empty");
+ return PathPatternRequestMatcher.withDefaults().matcher(path);
+ }
+
+ @Override
+ protected boolean matches(HttpServletRequest request, Supplier context) {
+ Assert.state(this.delegate != null, "'delegate' must not be null");
+ return this.delegate.matches(request);
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java
new file mode 100644
index 00000000000..441fff7c423
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+/**
+ * Auto-configuration for gRPC web security when using a reactive stack.
+ */
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure.security.web.servlet;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 00f402214f4..1b4dd838523 100644
--- a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -3,3 +3,5 @@ org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConf
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration
+org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerOAuth2ResourceServerAutoConfiguration
+org.springframework.boot.grpc.server.autoconfigure.security.GrpcServerSecurityAutoConfiguration
diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java
new file mode 100644
index 00000000000..418b257a765
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurerTests.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import java.util.HashMap;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.boot.grpc.server.GrpcServletRegistration;
+import org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer.GrpcCsrfRequestMatcher;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+
+/**
+ * Tests for {@link GrpcDisableCsrfHttpConfigurer}.
+ *
+ * @author Phillip Webb
+ */
+class GrpcDisableCsrfHttpConfigurerTests {
+
+ private GrpcDisableCsrfHttpConfigurer configurer = new GrpcDisableCsrfHttpConfigurer();
+
+ @Test
+ void initDisablesCsrf() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ addServiceDiscover(applicationContext);
+ addGrpcServletRegistration(applicationContext);
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ ArgumentCaptor matcher = ArgumentCaptor.captor();
+ then(csrf).should().requireCsrfProtectionMatcher(matcher.capture());
+ assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE);
+ }
+
+ @Test
+ void initWhenNoApplicationContextDoesNothing() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ then(csrf).should(never()).requireCsrfProtectionMatcher(any());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void initWhenNoCsrfConfigurerDoesNothing() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ addServiceDiscover(applicationContext);
+ addGrpcServletRegistration(applicationContext);
+ this.configurer.init(http);
+ CsrfConfigurer> csrfConfigurer = http.getConfigurer(CsrfConfigurer.class);
+ assertThat(csrfConfigurer).isNull();
+ }
+
+ @Test
+ void initWhenNoGrpcServiceDiscovererBeanDoesNothing() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ addGrpcServletRegistration(applicationContext);
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ then(csrf).should(never()).requireCsrfProtectionMatcher(any());
+ }
+
+ @Test
+ void initWhenNoGrpcServletRegistrationBeanDoesNothing() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ addServiceDiscover(applicationContext);
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ then(csrf).should(never()).requireCsrfProtectionMatcher(any());
+ }
+
+ @Test
+ void initWhenEnabledPropertyFalseDoesNothing() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=false").applyTo(applicationContext);
+ addServiceDiscover(applicationContext);
+ addGrpcServletRegistration(applicationContext);
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ then(csrf).should(never()).requireCsrfProtectionMatcher(any());
+ }
+
+ @Test
+ void initWhenEnabledPropertyTrueDisablesCsrf() {
+ ObjectPostProcessor objectPostProcessor = ObjectPostProcessor.identity();
+ AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor);
+ HttpSecurity http = new HttpSecurity(objectPostProcessor, authenticationBuilder, new HashMap<>());
+ StaticApplicationContext applicationContext = addApplicationContext(http);
+ TestPropertyValues.of("spring.grpc.server.security.csrf.enabled=true").applyTo(applicationContext);
+ addServiceDiscover(applicationContext);
+ addGrpcServletRegistration(applicationContext);
+ CsrfConfigurer> csrf = addCsrf(http);
+ this.configurer.init(http);
+ ArgumentCaptor matcher = ArgumentCaptor.captor();
+ then(csrf).should().requireCsrfProtectionMatcher(matcher.capture());
+ assertThat(matcher.getValue()).isSameAs(GrpcCsrfRequestMatcher.INSTANCE);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private CsrfConfigurer> addCsrf(HttpSecurity http) {
+ CsrfConfigurer> csrf = mock();
+ http.with((CsrfConfigurer) csrf);
+ return csrf;
+ }
+
+ private StaticApplicationContext addApplicationContext(HttpSecurity http) {
+ StaticApplicationContext applicationContext = new StaticApplicationContext();
+ http.setSharedObject(ApplicationContext.class, applicationContext);
+ return applicationContext;
+ }
+
+ private void addServiceDiscover(StaticApplicationContext applicationContext) {
+ GrpcServiceDiscoverer serviceDiscoverer = mock();
+ applicationContext.registerBean(GrpcServiceDiscoverer.class, serviceDiscoverer);
+ }
+
+ private void addGrpcServletRegistration(StaticApplicationContext applicationContext) {
+ GrpcServletRegistration servletRegistration = mock();
+ applicationContext.registerBean(GrpcServletRegistration.class, servletRegistration);
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java
new file mode 100644
index 00000000000..f20be2bf4ed
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerOAuth2ResourceServerAutoConfigurationTests.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import io.grpc.BindableService;
+import io.grpc.ServerServiceDefinition;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
+import org.springframework.boot.context.annotation.UserConfigurations;
+import org.springframework.boot.context.event.ApplicationFailedEvent;
+import org.springframework.boot.logging.LogLevel;
+import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration;
+import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration;
+import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
+import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
+import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
+import org.springframework.grpc.server.security.GrpcSecurity;
+import org.springframework.security.config.Customizer;
+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.web.configuration.EnableWebSecurity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link GrpcServerOAuth2ResourceServerAutoConfiguration}.
+ *
+ * @author Chris Bono
+ */
+class GrpcServerOAuth2ResourceServerAutoConfigurationTests {
+
+ private static final AutoConfigurations autoConfigurations = AutoConfigurations
+ .of(OAuth2ResourceServerAutoConfiguration.class, GrpcServerOAuth2ResourceServerAutoConfiguration.class);
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(autoConfigurations)
+ .withUserConfiguration(GrpcSecurityConfiguration.class)
+ .with(this::serviceBean)
+ .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
+
+ @Test
+ void jwtConfiguredWhenIssuerIsProvided() {
+ this.contextRunner
+ .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
+ .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
+ }
+
+ @Test
+ void jwtConfiguredWhenJwkSetIsProvided() {
+ this.contextRunner
+ .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000")
+ .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
+ }
+
+ @Test
+ void customInterceptorWhenJwkSetIsProvided() {
+ this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
+ .withConfiguration(UserConfigurations.of(CustomInterceptorConfiguration.class))
+ .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000")
+ .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class));
+ }
+
+ @Test
+ void notConfiguredWhenIssuerNotProvided() {
+ this.contextRunner
+ .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
+ }
+
+ @Test
+ void notConfiguredInWebApplication() {
+ new WebApplicationContextRunner().withConfiguration(autoConfigurations)
+ .withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class,
+ OAuth2ResourceServerAutoConfiguration.class))
+ .with(this::serviceBean)
+ .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
+ .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
+ }
+
+ @Test
+ void notConfiguredInWebApplicationWithNoBindableService() {
+ new WebApplicationContextRunner().withConfiguration(autoConfigurations)
+ .withConfiguration(AutoConfigurations.of(ServletWebSecurityAutoConfiguration.class,
+ OAuth2ResourceServerAutoConfiguration.class))
+ .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000")
+ .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class));
+ }
+
+ private , C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> R serviceBean(
+ R contextRunner) {
+ BindableService service = mock();
+ ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
+ given(service.bindService()).willReturn(serviceDefinition);
+ return contextRunner.withBean(BindableService.class, () -> service);
+ }
+
+ static class FailingApplicationFailedEventContext extends AnnotationConfigServletWebApplicationContext {
+
+ @Override
+ public void refresh() {
+ try {
+ super.refresh();
+ }
+ catch (Throwable ex) {
+ publishEvent(new ApplicationFailedEvent(new SpringApplication(this), new String[0], this, ex));
+ throw ex;
+ }
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class CustomInterceptorConfiguration {
+
+ @Bean
+ @GlobalServerInterceptor
+ AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception {
+ return grpc.authorizeRequests((requests) -> requests.allRequests().authenticated())
+ .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()))
+ .build();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableWebSecurity
+ static class GrpcSecurityConfiguration {
+
+ @Bean
+ GrpcSecurity grpcSecurity(ApplicationContext context, ObjectPostProcessor objectPostProcessor,
+ AuthenticationConfiguration authenticationConfiguration) {
+ AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration
+ .authenticationManagerBuilder(objectPostProcessor, context);
+ authenticationManagerBuilder
+ .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
+ return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context);
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java
new file mode 100644
index 00000000000..f3085d633a2
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServerSecurityAutoConfigurationTests.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security;
+
+import io.grpc.BindableService;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
+import org.springframework.boot.grpc.server.GrpcServletRegistration;
+import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider;
+import org.springframework.boot.logging.LogLevel;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.server.GrpcServerFactory;
+import org.springframework.grpc.server.exception.GrpcExceptionHandler;
+import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
+import org.springframework.grpc.server.security.AuthenticationProcessInterceptor;
+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.config.ObjectPostProcessor;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link GrpcServerSecurityAutoConfiguration}.
+ *
+ * @author Chris Bono
+ */
+class GrpcServerSecurityAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
+ .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
+
+ @Test
+ void whenSpringSecurityNotOnClasspathAutoConfigurationIsSkipped() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader(ObjectPostProcessor.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcServletCreatesGrpcSecurity() {
+ new WebApplicationContextRunner()
+ .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
+ .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
+ .withUserConfiguration(ServletConfiguration.class)
+ .run((context) -> {
+ assertThat(context).hasSingleBean(SecurityContextServerInterceptor.class);
+ assertThat(context).hasSingleBean(GrpcServerExecutorProvider.class);
+ });
+ }
+
+ @Test
+ void whenSpringGrpcAndSpringSecurityPresentAndUsingGrpcNativeCreatesGrpcSecurity() {
+ new ApplicationContextRunner()
+ .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
+ .withConfiguration(AutoConfigurations.of(GrpcServerSecurityAutoConfiguration.class))
+ .withUserConfiguration(NativeConfiguration.class)
+ .run((context) -> assertThat(context).hasSingleBean(GrpcSecurity.class));
+ }
+
+ @Test
+ void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
+ this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
+ this.contextRunner
+ .run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
+ this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true")
+ .run((context) -> assertThat(context).hasSingleBean(GrpcServerSecurityAutoConfiguration.class));
+ }
+
+ @Test
+ void grpcSecurityAutoConfiguredAsExpected() {
+ this.contextRunner.run((context) -> {
+ assertThat(context).getBean(GrpcExceptionHandler.class).isInstanceOf(SecurityGrpcExceptionHandler.class);
+ assertThat(context).getBean(AuthenticationProcessInterceptor.class).isNull();
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableWebSecurity
+ static class ServletConfiguration {
+
+ @Bean
+ GrpcServletRegistration grpcServletRegistration() {
+ return new GrpcServletRegistration(mock(), mock());
+ }
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) {
+ return http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll()).build();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableMethodSecurity
+ static class NativeConfiguration {
+
+ @Bean
+ GrpcServerFactory grpcServerFactory() {
+ return mock();
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java
new file mode 100644
index 00000000000..860e086745c
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/reactive/GrpcRequestTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security.web.reactive;
+
+import io.grpc.BindableService;
+import io.grpc.ServerServiceDefinition;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.grpc.server.autoconfigure.security.web.reactive.GrpcRequest.GrpcReactiveRequestMatcher;
+import org.springframework.boot.web.context.reactive.GenericReactiveWebApplicationContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.http.codec.ServerCodecConfigurer;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
+import org.springframework.web.server.adapter.DefaultServerWebExchange;
+import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
+import org.springframework.web.server.session.DefaultWebSessionManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link GrpcRequest}.
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ */
+class GrpcRequestTests {
+
+ private GenericReactiveWebApplicationContext context = new GenericReactiveWebApplicationContext();
+
+ @BeforeEach
+ void setup() {
+ MockService service1 = mock();
+ given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build());
+ MockService service2 = mock();
+ given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build());
+ this.context.registerBean("s1", BindableService.class, () -> service1);
+ this.context.registerBean("s2", BindableService.class, () -> service2);
+ this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context));
+ this.context.refresh();
+ }
+
+ @Test
+ void whenToAnyService() {
+ GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService();
+ assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
+ assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/notaservice")).isFalse();
+ }
+
+ @Test
+ void whenToAnyServiceWithExclude() {
+ GrpcReactiveRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service");
+ assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
+ assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse();
+ assertThat(isMatch(matcher, "/notaservice")).isFalse();
+ }
+
+ private boolean isMatch(GrpcReactiveRequestMatcher matcher, String path) {
+ MockExchange request = mockRequest(path);
+ MatchResult result = matcher.matches(request).block();
+ return (result != null) && result.isMatch();
+ }
+
+ private MockExchange mockRequest(String path) {
+ MockServerHttpRequest servletContext = MockServerHttpRequest.get(path).build();
+ MockExchange request = new MockExchange(servletContext, this.context);
+ return request;
+ }
+
+ interface MockService extends BindableService {
+
+ }
+
+ static class MockExchange extends DefaultServerWebExchange {
+
+ private ApplicationContext context;
+
+ MockExchange(MockServerHttpRequest request, ApplicationContext context) {
+ super(request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create(),
+ new AcceptHeaderLocaleContextResolver());
+ this.context = context;
+ }
+
+ @Override
+ public ApplicationContext getApplicationContext() {
+ return this.context;
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java
new file mode 100644
index 00000000000..87763de2a49
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/web/servlet/GrpcRequestTests.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2012-present 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.boot.grpc.server.autoconfigure.security.web.servlet;
+
+import io.grpc.BindableService;
+import io.grpc.ServerServiceDefinition;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest.GrpcServletRequestMatcher;
+import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Test for {@link GrpcRequest}.
+ *
+ * @author Dave Syer
+ * @author Phillip Webb
+ */
+class GrpcRequestTests {
+
+ private GenericWebApplicationContext context = new GenericWebApplicationContext();
+
+ @BeforeEach
+ void setup() {
+ MockService service1 = mock();
+ given(service1.bindService()).willReturn(ServerServiceDefinition.builder("my-service").build());
+ MockService service2 = mock();
+ given(service2.bindService()).willReturn(ServerServiceDefinition.builder("my-other-service").build());
+ this.context.registerBean("s1", BindableService.class, () -> service1);
+ this.context.registerBean("s2", BindableService.class, () -> service2);
+ this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context));
+ this.context.refresh();
+ }
+
+ @Test
+ void whenToAnyService() {
+ GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService();
+ assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
+ assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/my-other-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/notaservice")).isFalse();
+ }
+
+ @Test
+ void whenToAnyServiceWithExclude() {
+ GrpcServletRequestMatcher matcher = GrpcRequest.toAnyService().excluding("my-other-service");
+ assertThat(isMatch(matcher, "/my-service/Method")).isTrue();
+ assertThat(isMatch(matcher, "/my-service/Other")).isTrue();
+ assertThat(isMatch(matcher, "/my-other-service/Other")).isFalse();
+ assertThat(isMatch(matcher, "/notaservice")).isFalse();
+ }
+
+ private boolean isMatch(GrpcServletRequestMatcher matcher, String path) {
+ MockHttpServletRequest request = mockRequest(path);
+ return matcher.matches(request);
+ }
+
+ private MockHttpServletRequest mockRequest(String path) {
+ MockServletContext servletContext = new MockServletContext();
+ servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
+ MockHttpServletRequest request = new MockHttpServletRequest(servletContext);
+ request.setRequestURI(path);
+ return request;
+ }
+
+ interface MockService extends BindableService {
+
+ }
+
+}
diff --git a/settings.gradle b/settings.gradle
index 28dffcc3a6a..94d968336d5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -431,7 +431,9 @@ include ":smoke-test:spring-boot-smoke-test-grpc-client"
include ":smoke-test:spring-boot-smoke-test-grpc-client-test"
include ":smoke-test:spring-boot-smoke-test-grpc-server"
include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded"
+include ":smoke-test:spring-boot-smoke-test-grpc-server-oauth"
include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet"
+include ":smoke-test:spring-boot-smoke-test-grpc-server-secure"
include ":smoke-test:spring-boot-smoke-test-grpc-server-test"
include ":smoke-test:spring-boot-smoke-test-hateoas"
include ":smoke-test:spring-boot-smoke-test-hibernate"
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle
new file mode 100644
index 00000000000..758cc967819
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+plugins {
+ id "java"
+ id "com.google.protobuf" version "${protobufGradlePluginVersion}"
+ id "org.springframework.boot.docker-test"
+}
+
+description = "Spring Boot gRPC server oauth smoke test"
+
+dependencies {
+ implementation(project(":starter:spring-boot-starter-oauth2-authorization-server"))
+ implementation(project(":starter:spring-boot-starter-oauth2-resource-server"))
+ implementation(project(":starter:spring-boot-starter-grpc-server"))
+
+ testImplementation(project(":starter:spring-boot-starter-grpc-client"))
+ testImplementation(project(":starter:spring-boot-starter-grpc-test"))
+ testImplementation(project(":starter:spring-boot-starter-oauth2-client"))
+ testImplementation(project(":starter:spring-boot-starter-test"))
+}
+
+def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
+def grpcJava = dependenciesBom.getLibrary("Grpc Java")
+def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
+
+tasks.named("compileTestJava") {
+ options.nullability.checking = "tests"
+}
+
+nullability {
+ requireExplicitNullMarking = false
+}
+
+configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
+ extendsFrom(configurations.dependencyManagement)
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:${protobufJava.version}"
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
+ }
+ }
+ generateProtoTasks {
+ all()*.plugins {
+ grpc {
+ option '@generated=omit'
+ }
+ }
+ }
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java
new file mode 100644
index 00000000000..e7f8ca8ab90
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/HelloWorldService.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserveroauth;
+
+import io.grpc.stub.StreamObserver;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import smoketest.grpcserveroauth.proto.HelloReply;
+import smoketest.grpcserveroauth.proto.HelloRequest;
+import smoketest.grpcserveroauth.proto.HelloWorldGrpc;
+
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+@Service
+public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
+
+ private static Log logger = LogFactory.getLog(HelloWorldService.class);
+
+ @Override
+ public void sayHelloProfileScope(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloProfileScope", request, responseObserver);
+ }
+
+ @Override
+ public void sayHelloEmailScope(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloEmailScope", request, responseObserver);
+ }
+
+ @Override
+ public void sayHelloAuthenticated(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloAuthenticated", request, responseObserver);
+ }
+
+ public void sayHello(String methodName, HelloRequest request, StreamObserver responseObserver) {
+ String name = request.getName();
+ logger.info(methodName + " " + name);
+ Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
+ Assert.state(!name.startsWith("internal"), "Internal error");
+ String message = "%s '%s'".formatted(methodName, name);
+ HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java
new file mode 100644
index 00000000000..66cce5e1310
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserveroauth;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SampleGrpcServerOAuthApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleGrpcServerOAuthApplication.class, args);
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java
new file mode 100644
index 00000000000..6f7405ed52f
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/SecurityConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserveroauth;
+
+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.grpc.server.security.RequestMapperConfigurer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+@Configuration(proxyBeanMethods = false)
+public class SecurityConfiguration {
+
+ @Bean
+ @GlobalServerInterceptor
+ AuthenticationProcessInterceptor securityInterceptor(GrpcSecurity grpcSecurity) throws Exception {
+ return grpcSecurity.authorizeRequests(this::authorizeGrpcRequests)
+ .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()))
+ .build();
+ }
+
+ private void authorizeGrpcRequests(RequestMapperConfigurer requests) {
+ requests.methods("HelloWorld/SayHelloProfileScope").hasAuthority("SCOPE_profile");
+ requests.methods("HelloWorld/SayHelloEmailScope").hasAuthority("SCOPE_email");
+ requests.methods("HelloWorld/SayHelloAuthenticated").authenticated();
+ requests.methods("grpc.*/*").permitAll();
+ requests.allRequests().denyAll();
+ }
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) {
+ http.oauth2AuthorizationServer((authorizationServer) -> {
+ http.securityMatcher(authorizationServer.getEndpointsMatcher());
+ authorizationServer.oidc(withDefaults());
+ });
+ http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
+ http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll());
+ http.csrf((csrf) -> csrf.disable());
+ return http.build();
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java
new file mode 100644
index 00000000000..ab81b93206f
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/java/smoketest/grpcserveroauth/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+@NullMarked
+package smoketest.grpcserveroauth;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto
new file mode 100644
index 00000000000..e443b360171
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/proto/hello.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+option java_package = "smoketest.grpcserveroauth.proto";
+option java_multiple_files = true;
+
+service HelloWorld {
+ rpc SayHelloProfileScope (HelloRequest) returns (HelloReply) {}
+ rpc SayHelloEmailScope (HelloRequest) returns (HelloReply) {}
+ rpc SayHelloAuthenticated (HelloRequest) returns (HelloReply) {}
+}
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string message = 1;
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml
new file mode 100644
index 00000000000..c9257e42c1e
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/main/resources/application.yaml
@@ -0,0 +1,21 @@
+logging.level.org.springframework.security: TRACE
+spring:
+ security:
+ oauth2:
+ authorizationserver:
+ client:
+ oidc-client:
+ registration:
+ client-id: "spring"
+ client-secret: "{noop}secret"
+ client-authentication-methods:
+ - "client_secret_basic"
+ authorization-grant-types:
+ - "client_credentials"
+ - "refresh_token"
+ scopes:
+ - "openid"
+ - "profile"
+ resourceserver:
+ jwt:
+ jwk-set-uri: http://localhost:8080/oauth2/jwks
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java
new file mode 100644
index 00000000000..9bfeb8bab90
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-oauth/src/test/java/smoketest/grpcserveroauth/SampleGrpcServerOAuthApplicationTests.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserveroauth;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.Status.Code;
+import io.grpc.StatusRuntimeException;
+import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.stub.StreamObserver;
+import org.awaitility.Awaitility;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import smoketest.grpcserveroauth.proto.HelloReply;
+import smoketest.grpcserveroauth.proto.HelloRequest;
+import smoketest.grpcserveroauth.proto.HelloWorldGrpc.HelloWorldBlockingStub;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+import org.springframework.grpc.client.ImportGrpcClients;
+import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor;
+import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
+import org.springframework.security.oauth2.client.endpoint.RestClientClientCredentialsTokenResponseClient;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.SupplierClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+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.test.annotation.DirtiesContext;
+import org.springframework.util.function.SingletonSupplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
+ properties = { "spring.grpc.server.port=0",
+ "spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}",
+ "spring.grpc.client.channel.oauth.target=static://localhost:${local.grpc.server.port}" })
+@DirtiesContext
+class SampleGrpcServerOAuthApplicationTests {
+
+ @Autowired
+ @Qualifier("unauthenticatedHelloWorldBlockingStub")
+ private HelloWorldBlockingStub unuathenticated;
+
+ @Autowired
+ @Qualifier("oauthHelloWorldBlockingStub")
+ private HelloWorldBlockingStub oauth;
+
+ @Autowired
+ private ServerReflectionStub reflection;
+
+ @Test
+ void whenUnauthenticatedStub() {
+ HelloWorldBlockingStub stub = this.unuathenticated;
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloProfileScope))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAuthenticated))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertCanInvokeReflection();
+ }
+
+ @Test
+ void whenOAuth() {
+ HelloWorldBlockingStub stub = this.oauth;
+ assertThat(invoke(stub::sayHelloProfileScope)).isEqualTo("sayHelloProfileScope 'Spring'");
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloEmailScope))
+ .satisfies(statusCode(Code.PERMISSION_DENIED));
+ assertThat(invoke(stub::sayHelloAuthenticated)).isEqualTo("sayHelloAuthenticated 'Spring'");
+ assertCanInvokeReflection();
+ }
+
+ private String invoke(Function method) {
+ HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
+ return method.apply(request).getMessage();
+ }
+
+ private void assertCanInvokeReflection() {
+ ObservedResponse response = invokeReflection();
+ assertThat(response.getValue()).isNotNull();
+ assertThat(response.getError()).isNull();
+ }
+
+ private ObservedResponse invokeReflection() {
+ ObservedResponse response = new ObservedResponse<>();
+ StreamObserver request = this.reflection.serverReflectionInfo(response);
+ request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build());
+ request.onCompleted();
+ response.await();
+ return response;
+ }
+
+ private Consumer statusCode(Code expected) {
+ return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected);
+ }
+
+ @TestConfiguration(proxyBeanMethods = false)
+ @ImportGrpcClients(types = ServerReflectionStub.class)
+ @ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class)
+ @ImportGrpcClients(target = "oauth", prefix = "oauth", types = HelloWorldBlockingStub.class)
+ static class GrpcClientTestConfiguration {
+
+ @Bean
+ > GrpcChannelBuilderCustomizer channelSecurityCustomizer(
+ ObjectProvider clientRegistrationRepository) {
+ return GrpcChannelBuilderCustomizer.matching("oauth", (builder) -> {
+ Supplier tokenSupplier = SingletonSupplier
+ .of(() -> token(clientRegistrationRepository.getObject()));
+ builder.intercept(new BearerTokenAuthenticationInterceptor(tokenSupplier));
+ });
+ }
+
+ private String token(ClientRegistrationRepository clientRegistrationRepository) {
+ RestClientClientCredentialsTokenResponseClient client = new RestClientClientCredentialsTokenResponseClient();
+ ClientRegistration registration = clientRegistrationRepository.findByRegistrationId("spring");
+ OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
+ return client.getTokenResponse(request).getAccessToken().getTokenValue();
+ }
+
+ @Bean
+ ClientRegistrationRepository lazyClientRegistrationRepository(Environment environment) {
+ return new SupplierClientRegistrationRepository(() -> getClientRegistrationRepository(environment));
+ }
+
+ private InMemoryClientRegistrationRepository getClientRegistrationRepository(Environment environment) {
+ return new InMemoryClientRegistrationRepository(ClientRegistration.withRegistrationId("spring")
+ .clientId("spring")
+ .clientSecret("secret")
+ .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+ .scope("profile")
+ .tokenUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/token"))
+ .build());
+ }
+
+ @Bean
+ SupplierJwtDecoder lazyJwtDecoder(Environment environment) {
+ return new SupplierJwtDecoder(() -> getJwtDecoder(environment));
+ }
+
+ private JwtDecoder getJwtDecoder(Environment environment) {
+ JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder
+ .withJwkSetUri(environment.resolvePlaceholders("http://localhost:${local.server.port}/oauth2/jwks"));
+ builder.jwsAlgorithms((algorithms) -> algorithms.add(SignatureAlgorithm.from("RS256")));
+ return builder.build();
+ }
+
+ }
+
+ static class ObservedResponse implements StreamObserver {
+
+ private volatile @Nullable T value;
+
+ private volatile @Nullable Throwable error;
+
+ @Override
+ public synchronized void onNext(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public synchronized void onError(Throwable error) {
+ this.error = error;
+ }
+
+ @Override
+ public void onCompleted() {
+ }
+
+ void await() {
+ Awaitility.await().until(this::hasResponse);
+ }
+
+ private synchronized boolean hasResponse() {
+ return this.value != null || this.error != null;
+ }
+
+ @Nullable T getValue() {
+ return this.value;
+ }
+
+ @Nullable Throwable getError() {
+ return this.error;
+ }
+
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle
new file mode 100644
index 00000000000..a4ec289d2f2
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+plugins {
+ id "java"
+ id "com.google.protobuf" version "${protobufGradlePluginVersion}"
+ id "org.springframework.boot.docker-test"
+}
+
+description = "Spring Boot gRPC server secure smoke test"
+
+dependencies {
+ implementation(project(":starter:spring-boot-starter-grpc-server"))
+ implementation(project(":starter:spring-boot-starter-security"))
+
+ testImplementation(project(":starter:spring-boot-starter-grpc-client"))
+ testImplementation(project(":starter:spring-boot-starter-grpc-test"))
+ testImplementation(project(":starter:spring-boot-starter-test"))
+}
+
+def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
+def grpcJava = dependenciesBom.getLibrary("Grpc Java")
+def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
+
+tasks.named("compileTestJava") {
+ options.nullability.checking = "tests"
+}
+
+nullability {
+ requireExplicitNullMarking = false
+}
+
+configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
+ extendsFrom(configurations.dependencyManagement)
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:${protobufJava.version}"
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
+ }
+ }
+ generateProtoTasks {
+ all()*.plugins {
+ grpc {
+ option '@generated=omit'
+ }
+ }
+ }
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java
new file mode 100644
index 00000000000..3330d1a1555
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/HelloWorldService.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserversecure;
+
+import io.grpc.stub.StreamObserver;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import smoketest.grpcserversecure.proto.HelloReply;
+import smoketest.grpcserversecure.proto.HelloRequest;
+import smoketest.grpcserversecure.proto.HelloWorldGrpc;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+@Service
+public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
+
+ private static Log logger = LogFactory.getLog(HelloWorldService.class);
+
+ @Override
+ public void sayHelloAdmin(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloAdmin", request, responseObserver);
+ }
+
+ @Override
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ public void sayHelloAdminAnnotated(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloAdminAnnotated", request, responseObserver);
+ }
+
+ @Override
+ public void sayHelloUser(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloUser", request, responseObserver);
+ }
+
+ @Override
+ @PreAuthorize("hasAuthority('ROLE_USER')")
+ public void sayHelloUserAnnotated(HelloRequest request, StreamObserver responseObserver) {
+ sayHello("sayHelloUserAnnotated", request, responseObserver);
+ }
+
+ public void sayHello(String methodName, HelloRequest request, StreamObserver responseObserver) {
+ String name = request.getName();
+ logger.info(methodName + " " + name);
+ Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
+ Assert.state(!name.startsWith("internal"), "Internal error");
+ String message = "%s '%s'".formatted(methodName, name);
+ HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java
new file mode 100644
index 00000000000..9f92bc74883
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserversecure;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SampleGrpcServerSecureApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleGrpcServerSecureApplication.class, args);
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java
new file mode 100644
index 00000000000..7beaa3c3dd8
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/SecurityConfiguration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserversecure;
+
+import io.grpc.ServerInterceptor;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+import org.springframework.grpc.server.security.GrpcSecurity;
+import org.springframework.grpc.server.security.RequestMapperConfigurer;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+@Configuration(proxyBeanMethods = false)
+public class SecurityConfiguration {
+
+ @Bean
+ InMemoryUserDetailsManager inMemoryUserDetailsManager() {
+ UserDetails user = user("user", "{noop}userpassword", "ROLE_USER");
+ UserDetails admin = user("admin", "{noop}adminpassword", "ROLE_ADMIN");
+ return new InMemoryUserDetailsManager(user, admin);
+ }
+
+ private UserDetails user(String username, String password, String authority) {
+ return User.withUsername(username).password(password).authorities(authority).build();
+ }
+
+ @Bean
+ @GlobalServerInterceptor
+ ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception {
+ return security.authorizeRequests(this::authorizeRequests)
+ .httpBasic(withDefaults())
+ .preauth(withDefaults())
+ .build();
+ }
+
+ private void authorizeRequests(RequestMapperConfigurer requests) {
+ requests.methods("HelloWorld/SayHelloAdmin").hasAuthority("ROLE_ADMIN");
+ requests.methods("HelloWorld/SayHelloAdminAnnotated").hasAuthority("ROLE_ADMIN");
+ requests.methods("HelloWorld/SayHelloUser").hasAuthority("ROLE_USER");
+ requests.methods("HelloWorld/SayHelloUserAnnotated").hasAuthority("ROLE_USER");
+ requests.methods("grpc.*/*").permitAll();
+ requests.allRequests().denyAll();
+ }
+
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java
new file mode 100644
index 00000000000..7f019548742
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/java/smoketest/grpcserversecure/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-present 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.
+ */
+
+@NullMarked
+package smoketest.grpcserversecure;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto
new file mode 100644
index 00000000000..700ee4ac930
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/main/proto/hello.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+option java_package = "smoketest.grpcserversecure.proto";
+option java_multiple_files = true;
+
+service HelloWorld {
+ rpc SayHelloAdmin (HelloRequest) returns (HelloReply) {}
+ rpc SayHelloAdminAnnotated (HelloRequest) returns (HelloReply) {}
+ rpc SayHelloUser (HelloRequest) returns (HelloReply) {}
+ rpc SayHelloUserAnnotated (HelloRequest) returns (HelloReply) {}
+}
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string message = 1;
+}
diff --git a/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java
new file mode 100644
index 00000000000..4c7dab0e33d
--- /dev/null
+++ b/smoke-test/spring-boot-smoke-test-grpc-server-secure/src/test/java/smoketest/grpcserversecure/SampleGrpcServerSecureApplicationTests.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2012-present 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 smoketest.grpcserversecure;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.Status.Code;
+import io.grpc.StatusRuntimeException;
+import io.grpc.reflection.v1.ServerReflectionGrpc.ServerReflectionStub;
+import io.grpc.reflection.v1.ServerReflectionRequest;
+import io.grpc.reflection.v1.ServerReflectionResponse;
+import io.grpc.stub.StreamObserver;
+import org.awaitility.Awaitility;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import smoketest.grpcserversecure.proto.HelloReply;
+import smoketest.grpcserversecure.proto.HelloRequest;
+import smoketest.grpcserversecure.proto.HelloWorldGrpc.HelloWorldBlockingStub;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+import org.springframework.grpc.client.ImportGrpcClients;
+import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor;
+import org.springframework.test.annotation.DirtiesContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+@SpringBootTest(properties = { "spring.grpc.server.port=0",
+ "spring.grpc.client.channel.default.target=static://localhost:${local.grpc.server.port}",
+ "spring.grpc.client.channel.user.target=static://localhost:${local.grpc.server.port}",
+ "spring.grpc.client.channel.admin.target=static://localhost:${local.grpc.server.port}" })
+@DirtiesContext
+class SampleGrpcServerSecureApplicationTests {
+
+ @Autowired
+ @Qualifier("unauthenticatedHelloWorldBlockingStub")
+ private HelloWorldBlockingStub unuathenticated;
+
+ @Autowired
+ @Qualifier("userAuthenticatedHelloWorldBlockingStub")
+ private HelloWorldBlockingStub userAuthenticated;
+
+ @Autowired
+ @Qualifier("adminAuthenticatedHelloWorldBlockingStub")
+ private HelloWorldBlockingStub adminAuthenticated;
+
+ @Autowired
+ private ServerReflectionStub reflection;
+
+ @Test
+ void whenUnauthenticatedStub() {
+ HelloWorldBlockingStub stub = this.unuathenticated;
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated))
+ .satisfies(statusCode(Code.UNAUTHENTICATED));
+ assertCanInvokeReflection();
+ }
+
+ @Test
+ void whenUserAuthenticatedStub() {
+ HelloWorldBlockingStub stub = this.userAuthenticated;
+ assertThat(invoke(stub::sayHelloUser)).isEqualTo("sayHelloUser 'Spring'");
+ assertThat(invoke(stub::sayHelloUserAnnotated)).isEqualTo("sayHelloUserAnnotated 'Spring'");
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdmin))
+ .satisfies(statusCode(Code.PERMISSION_DENIED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloAdminAnnotated))
+ .satisfies(statusCode(Code.PERMISSION_DENIED));
+ assertCanInvokeReflection();
+ }
+
+ @Test
+ void whenAdminAuthenticatedStub() {
+ HelloWorldBlockingStub stub = this.adminAuthenticated;
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUser))
+ .satisfies(statusCode(Code.PERMISSION_DENIED));
+ assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() -> invoke(stub::sayHelloUserAnnotated))
+ .satisfies(statusCode(Code.PERMISSION_DENIED));
+ assertThat(invoke(stub::sayHelloAdmin)).isEqualTo("sayHelloAdmin 'Spring'");
+ assertThat(invoke(stub::sayHelloAdminAnnotated)).isEqualTo("sayHelloAdminAnnotated 'Spring'");
+ assertCanInvokeReflection();
+ }
+
+ private String invoke(Function method) {
+ HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
+ return method.apply(request).getMessage();
+ }
+
+ private void assertCanInvokeReflection() {
+ ObservedResponse response = invokeReflection();
+ assertThat(response.getValue()).isNotNull();
+ assertThat(response.getError()).isNull();
+ }
+
+ private ObservedResponse invokeReflection() {
+ ObservedResponse response = new ObservedResponse<>();
+ StreamObserver request = this.reflection.serverReflectionInfo(response);
+ request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build());
+ request.onCompleted();
+ response.await();
+ return response;
+ }
+
+ private Consumer statusCode(Code expected) {
+ return (ex) -> assertThat(ex).extracting("status.code").isEqualTo(expected);
+ }
+
+ @TestConfiguration(proxyBeanMethods = false)
+ @ImportGrpcClients(types = ServerReflectionStub.class)
+ @ImportGrpcClients(prefix = "unauthenticated", types = HelloWorldBlockingStub.class)
+ @ImportGrpcClients(target = "user", prefix = "userAuthenticated", types = HelloWorldBlockingStub.class)
+ @ImportGrpcClients(target = "admin", prefix = "adminAuthenticated", types = HelloWorldBlockingStub.class)
+ static class GrpcClientTestConfiguration {
+
+ @Bean
+ > GrpcChannelBuilderCustomizer channelSecurityCustomizer() {
+ return (target, builder) -> {
+ if ("user".equals(target)) {
+ builder.intercept(new BasicAuthenticationInterceptor("user", "userpassword"));
+ }
+ if ("admin".equals(target)) {
+ builder.intercept(new BasicAuthenticationInterceptor("admin", "adminpassword"));
+ }
+ };
+ }
+
+ }
+
+ static class ObservedResponse implements StreamObserver {
+
+ private volatile @Nullable T value;
+
+ private volatile @Nullable Throwable error;
+
+ @Override
+ public synchronized void onNext(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public synchronized void onError(Throwable error) {
+ this.error = error;
+ }
+
+ @Override
+ public void onCompleted() {
+ }
+
+ void await() {
+ Awaitility.await().until(this::hasResponse);
+ }
+
+ private synchronized boolean hasResponse() {
+ return this.value != null || this.error != null;
+ }
+
+ @Nullable T getValue() {
+ return this.value;
+ }
+
+ @Nullable Throwable getError() {
+ return this.error;
+ }
+
+ }
+
+}