diff --git a/samples/grpc-tomcat-secure/README.md b/samples/grpc-tomcat-secure/README.md
new file mode 100644
index 0000000..975f063
--- /dev/null
+++ b/samples/grpc-tomcat-secure/README.md
@@ -0,0 +1,61 @@
+# Spring Boot gRPC Sample
+
+This project is a copy one of the samples from the [gRPC Spring Boot Starter](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/examples/local-grpc-server/build.gradle). Build and run any way you like to run Spring Boot. E.g:
+
+```
+$ ./mvnw spring-boot:run
+...
+ . ____ _ __ _ _
+ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot :: (v3.0.0)
+
+2022-12-08T05:32:24.934-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 551632 (/home/dsyer/dev/scratch/demo/target/classes started by dsyer in /home/dsyer/dev/scratch/demo)
+2022-12-08T05:32:24.938-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
+2022-12-08T05:32:25.377-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration
+2022-12-08T05:32:25.416-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
+2022-12-08T05:32:25.425-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration
+2022-12-08T05:32:25.427-08:00 INFO 551632 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory
+2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService
+2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
+2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
+2022-12-08T05:32:25.820-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
+2022-12-08T05:32:25.831-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.264 seconds (process running for 1.623)
+```
+
+The server starts by default on port 9090. Test with [gRPCurl](https://github.com/fullstorydev/grpcurl):
+
+```
+$ grpcurl -d '{"name":"Hi"}' -plaintext localhost:9090 Simple.SayHello
+{
+ "message": "Hello ==\u003e Hi"
+}
+```
+
+## Native Image
+
+The app compiles to a native image if the JVM is GraalVM:
+
+```
+$ ./mvnw -Pnative native:compile
+$ ./target/demo
+ . ____ _ __ _ _
+ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot :: (v3.0.0)
+
+2022-12-08T05:36:54.365-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.5 with PID 554359 (/home/dsyer/dev/scratch/demo/target/demo started by dsyer in /home/dsyer/dev/scratch/demo)
+2022-12-08T05:36:54.366-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
+2022-12-08T05:36:54.377-08:00 INFO 554359 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory
+2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService
+2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
+2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
+2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
+2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.046 seconds (process running for 0.052)
+```
diff --git a/samples/grpc-tomcat-secure/pom.xml b/samples/grpc-tomcat-secure/pom.xml
new file mode 100644
index 0000000..1c70226
--- /dev/null
+++ b/samples/grpc-tomcat-secure/pom.xml
@@ -0,0 +1,183 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.0
+
+
+ org.springframework.grpc
+ grpc-tomcat-secure-sample
+ 0.3.0-SNAPSHOT
+ Spring gRPC Server Sample
+ Demo project for Spring gRPC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17
+ 0.0.39
+ 3.25.5
+ 1.63.2
+
+
+
+
+ org.springframework.grpc
+ spring-grpc-dependencies
+ 0.3.0-SNAPSHOT
+ pom
+ import
+
+
+
+
+
+ org.springframework.grpc
+ spring-grpc-spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ io.grpc
+ grpc-services
+
+
+ io.grpc
+ grpc-servlet-jakarta
+
+
+ org.springframework.grpc
+ spring-grpc-test
+ test
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+ --verbose
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+ ${spring-javaformat-maven-plugin.version}
+
+
+
+ validate
+ true
+
+ validate
+
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+ jakarta_omit,@generated=omit
+
+
+ com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}
+ grpc-java
+
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ false
+
+
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java
new file mode 100644
index 0000000..007947e
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java
@@ -0,0 +1,25 @@
+package org.springframework.grpc.sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@SpringBootApplication
+public class GrpcServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(GrpcServerApplication.class, args);
+ }
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return http.httpBasic(Customizer.withDefaults())
+ .authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
+ .csrf(csrf -> csrf.disable())
+ .build();
+ }
+
+}
diff --git a/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java
new file mode 100644
index 0000000..06d0526
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java
@@ -0,0 +1,48 @@
+package org.springframework.grpc.sample;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.grpc.sample.proto.HelloReply;
+import org.springframework.grpc.sample.proto.HelloRequest;
+import org.springframework.grpc.sample.proto.SimpleGrpc;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+import io.grpc.stub.StreamObserver;
+
+@Service
+public class GrpcServerService extends SimpleGrpc.SimpleImplBase {
+
+ private static Log log = LogFactory.getLog(GrpcServerService.class);
+
+ @Override
+ public void sayHello(HelloRequest req, StreamObserver responseObserver) {
+ log.info("Hello " + req.getName());
+ log.info("Security " + SecurityContextHolder.getContext().getAuthentication());
+ HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void streamHello(HelloRequest req, StreamObserver responseObserver) {
+ log.info("Hello " + req.getName());
+ int count = 0;
+ while (count < 10) {
+ HelloReply reply = HelloReply.newBuilder().setMessage("Hello(" + count + ") ==> " + req.getName()).build();
+ responseObserver.onNext(reply);
+ count++;
+ try {
+ Thread.sleep(1000L);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ responseObserver.onError(e);
+ return;
+ }
+ }
+ responseObserver.onCompleted();
+ }
+
+}
\ No newline at end of file
diff --git a/samples/grpc-tomcat-secure/src/main/proto/hello.proto b/samples/grpc-tomcat-secure/src/main/proto/hello.proto
new file mode 100644
index 0000000..731679c
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/main/proto/hello.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "org.springframework.grpc.sample.proto";
+option java_outer_classname = "HelloWorldProto";
+
+// The greeting service definition.
+service Simple {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {
+ }
+ rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+ string message = 1;
+}
\ No newline at end of file
diff --git a/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties b/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties
new file mode 100644
index 0000000..5c43451
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/main/resources/META-INF/native-image/org.springframework.samples/grpc-tomcat-sample/native-image.properties
@@ -0,0 +1,2 @@
+# Ignored unless building in Nix (https://github.com/oracle/graal/issues/8639)
+Args = -ENIX_LDFLAGS -ENIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu
diff --git a/samples/grpc-tomcat-secure/src/main/resources/application.properties b/samples/grpc-tomcat-secure/src/main/resources/application.properties
new file mode 100644
index 0000000..dc54139
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+spring.application.name=grpc-tomcat-secure
+server.port=9090
+server.http2.enabled=true
+spring.security.user.name=user
+spring.security.user.password=user
\ No newline at end of file
diff --git a/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java
new file mode 100644
index 0000000..f08b750
--- /dev/null
+++ b/samples/grpc-tomcat-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java
@@ -0,0 +1,108 @@
+package org.springframework.grpc.sample;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Base64;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.grpc.client.ChannelBuilderOptions;
+import org.springframework.grpc.client.GrpcChannelFactory;
+import org.springframework.grpc.sample.proto.HelloReply;
+import org.springframework.grpc.sample.proto.HelloRequest;
+import org.springframework.grpc.sample.proto.SimpleGrpc;
+import org.springframework.grpc.server.security.GrpcSecurity;
+import org.springframework.grpc.test.LocalGrpcPort;
+import org.springframework.test.annotation.DirtiesContext;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
+import io.grpc.Status.Code;
+import io.grpc.MethodDescriptor;
+import io.grpc.StatusRuntimeException;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class GrpcServerApplicationTests {
+
+ private static Log log = LogFactory.getLog(GrpcServerApplicationTests.class);
+
+ public static void main(String[] args) {
+ new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class).run(args);
+ }
+
+ @Autowired
+ @Qualifier("stub")
+ private SimpleGrpc.SimpleBlockingStub stub;
+
+ @Autowired
+ @Qualifier("basic")
+ private SimpleGrpc.SimpleBlockingStub basic;
+
+ @Test
+ @DirtiesContext
+ void contextLoads() {
+ }
+
+ @Test
+ @DirtiesContext
+ void unauthenticated() {
+ StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
+ () -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build()));
+ assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode());
+ }
+
+ @Test
+ @DirtiesContext
+ void authenticated() {
+ log.info("Testing");
+ HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build());
+ assertEquals("Hello ==> Alien", response.getMessage());
+ }
+
+ @TestConfiguration
+ static class ExtraConfiguration {
+
+ @Bean
+ @Lazy
+ SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels, @LocalServerPort int port) {
+ return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port,
+ ChannelBuilderOptions.defaults().withCustomizer((__, channel) -> {
+ channel.intercept(new ClientInterceptor() {
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor method, CallOptions callOptions, Channel next) {
+ return new SimpleForwardingClientCall(next.newCall(method, callOptions)) {
+ public void start(ClientCall.Listener responseListener,
+ io.grpc.Metadata headers) {
+ headers.put(GrpcSecurity.AUTHORIZATION_KEY,
+ "Basic " + Base64.getEncoder().encodeToString("user:user".getBytes()));
+ super.start(responseListener, headers);
+ };
+ };
+ }
+ });
+ })));
+ }
+
+ @Bean
+ @Lazy
+ SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalServerPort int port) {
+ return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port));
+ }
+
+ }
+
+}
diff --git a/samples/grpc-tomcat/src/main/resources/application.properties b/samples/grpc-tomcat/src/main/resources/application.properties
index 28f590b..735467d 100644
--- a/samples/grpc-tomcat/src/main/resources/application.properties
+++ b/samples/grpc-tomcat/src/main/resources/application.properties
@@ -2,4 +2,5 @@ spring.application.name=grpc-tomcat
server.port=9090
server.http2.enabled=true
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
+logging.level.org.springframework.security=DEBUG
management.endpoints.web.exposure.include=*
diff --git a/samples/pom.xml b/samples/pom.xml
index 32d1733..cd413f8 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -19,6 +19,7 @@
grpc-server
grpc-server-netty-shaded
grpc-tomcat
+ grpc-tomcat-secure
grpc-webflux
diff --git a/spring-grpc-core/pom.xml b/spring-grpc-core/pom.xml
index cb80f54..08bee50 100644
--- a/spring-grpc-core/pom.xml
+++ b/spring-grpc-core/pom.xml
@@ -27,6 +27,11 @@
org.springframework
spring-context
+
+ org.springframework.security
+ spring-security-web
+ true
+
io.grpc
grpc-netty
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java
new file mode 100644
index 0000000..34bcdf2
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/GrpcSecurity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.server.security;
+
+import io.grpc.Metadata;
+
+/**
+ * Defines constants and utilities for working with gRPC security.
+ *
+ * @author Dave Syer
+ */
+public final class GrpcSecurity {
+
+ /**
+ * Constant for the Authorization header.
+ */
+ public static final Metadata.Key AUTHORIZATION_KEY = Metadata.Key.of("Authorization",
+ Metadata.ASCII_STRING_MARSHALLER);
+
+ private GrpcSecurity() {
+ }
+
+}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java
new file mode 100644
index 0000000..f879d80
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityContextServerInterceptor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.server.security;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+
+public class SecurityContextServerInterceptor implements ServerInterceptor {
+
+ @Override
+ public Listener interceptCall(ServerCall call, Metadata headers,
+ ServerCallHandler next) {
+ SecurityContext securityContext = SecurityContextHolder.getContext();
+ return new SecurityContextHandlerListener(next.startCall(call, headers), securityContext);
+ }
+
+ static class SecurityContextHandlerListener extends SimpleForwardingServerCallListener {
+
+ private SecurityContext securityContext;
+
+ SecurityContextHandlerListener(ServerCall.Listener delegate, SecurityContext securityContext) {
+ super(delegate);
+ this.securityContext = securityContext;
+ }
+
+ @Override
+ public void onMessage(ReqT message) {
+ SecurityContextHolder.setContext(this.securityContext);
+ try {
+ super.onMessage(message);
+ }
+ finally {
+ SecurityContextHolder.clearContext();
+ }
+ }
+
+ @Override
+ public void onHalfClose() {
+ SecurityContextHolder.setContext(this.securityContext);
+ try {
+ super.onHalfClose();
+ }
+ finally {
+ SecurityContextHolder.clearContext();
+ }
+ }
+
+ @Override
+ public void onReady() {
+ SecurityContextHolder.setContext(this.securityContext);
+ try {
+ super.onReady();
+ }
+ finally {
+ SecurityContextHolder.clearContext();
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ SecurityContextHolder.clearContext();
+ }
+
+ @Override
+ public void onComplete() {
+ super.onComplete();
+ SecurityContextHolder.clearContext();
+ }
+
+ }
+
+}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java
new file mode 100644
index 0000000..24b1f38
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/security/SecurityGrpcExceptionHandler.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.server.security;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.grpc.server.exception.GrpcExceptionHandler;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.AuthenticationException;
+
+import io.grpc.Status;
+
+public class SecurityGrpcExceptionHandler implements GrpcExceptionHandler {
+
+ private static final Log logger = LogFactory.getLog(SecurityGrpcExceptionHandler.class);
+
+ @Override
+ public Status handleException(Throwable exception) {
+ if (exception instanceof AuthenticationException) {
+ if (logger.isDebugEnabled()) {
+ logger.error("Failed to authenticate", exception);
+ }
+ return Status.UNAUTHENTICATED.withDescription(exception.getMessage());
+ }
+ if (exception instanceof AccessDeniedException) {
+ if (logger.isDebugEnabled()) {
+ logger.error("Failed to authorize", exception);
+ }
+ return Status.PERMISSION_DENIED.withDescription(exception.getMessage());
+ }
+ return null;
+ }
+
+}
diff --git a/spring-grpc-dependencies/pom.xml b/spring-grpc-dependencies/pom.xml
index 21c783c..dab5cdb 100644
--- a/spring-grpc-dependencies/pom.xml
+++ b/spring-grpc-dependencies/pom.xml
@@ -50,6 +50,7 @@
6.2.1
+ 6.4.1
4.1.115.Final
1.69.0
3.25.5
@@ -87,6 +88,13 @@
pom
import
+
+ org.springframework.security
+ spring-security-bom
+ ${spring-security.version}
+ pom
+ import
+
io.netty
netty-bom
diff --git a/spring-grpc-spring-boot-autoconfigure/pom.xml b/spring-grpc-spring-boot-autoconfigure/pom.xml
index 5e90e66..b702d6e 100644
--- a/spring-grpc-spring-boot-autoconfigure/pom.xml
+++ b/spring-grpc-spring-boot-autoconfigure/pom.xml
@@ -105,6 +105,16 @@
micrometer-core
true
+
+ org.springframework.security
+ spring-security-config
+ true
+
+
+ org.springframework.security
+ spring-security-web
+ true
+
org.springframework.boot
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java
index e8755e8..cbed6c8 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java
@@ -99,7 +99,7 @@ public class GrpcServerFactoryAutoConfiguration {
}
- static class OnGrpcServletCondition extends AllNestedConditions {
+ public static class OnGrpcServletCondition extends AllNestedConditions {
OnGrpcServletCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
@@ -123,7 +123,7 @@ public class GrpcServerFactoryAutoConfiguration {
}
- static class OnNativeGrpcServerCondition extends AnyNestedCondition {
+ public static class OnNativeGrpcServerCondition extends AnyNestedCondition {
OnNativeGrpcServerCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java
new file mode 100644
index 0000000..bb1da18
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/security/GrpcSecurityAutoConfiguration.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.autoconfigure.server.security;
+
+import java.util.concurrent.Executors;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration;
+import org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+import org.springframework.grpc.server.exception.GrpcExceptionHandler;
+import org.springframework.grpc.server.security.SecurityContextServerInterceptor;
+import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler;
+import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
+import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;
+import org.springframework.security.config.ObjectPostProcessor;
+import org.springframework.security.web.SecurityFilterChain;
+
+import io.grpc.ServerBuilder;
+
+@ConditionalOnClass(ObjectPostProcessor.class)
+@AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, after = SecurityAutoConfiguration.class)
+public class GrpcSecurityAutoConfiguration {
+
+ @Configuration(proxyBeanMethods = false)
+ static class ExceptionHandlerAutoConfiguration {
+
+ @Bean
+ public GrpcExceptionHandler accessExceptionHandler() {
+ return new SecurityGrpcExceptionHandler();
+ }
+
+ }
+
+ @ConditionalOnBean(SecurityFilterChain.class)
+ @Configuration(proxyBeanMethods = false)
+ @Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class)
+ static class GrpcSecurityConfigurerAutoConfiguration {
+
+ @Bean
+ @GlobalServerInterceptor
+ public SecurityContextServerInterceptor securityContextInterceptor() {
+ return new SecurityContextServerInterceptor();
+ }
+
+ @Bean
+ public > ServerBuilderCustomizer securityContextCustomizer() {
+ // TODO: configure the thread pool via GrpcServerProperties?
+ return (serverBuilder) -> serverBuilder.executor(new DelegatingSecurityContextExecutor(
+ Executors.newCachedThreadPool(new CustomizableThreadFactory("grpc-server-"))));
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index cee7caf..27ce109 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -6,3 +6,4 @@ org.springframework.grpc.autoconfigure.server.health.GrpcServerHealthAutoConfigu
org.springframework.grpc.autoconfigure.server.GrpcServerObservationAutoConfiguration
org.springframework.grpc.autoconfigure.server.GrpcServerReflectionAutoConfiguration
org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration
+org.springframework.grpc.autoconfigure.server.security.GrpcSecurityAutoConfiguration