diff --git a/README.adoc b/README.adoc index db1f4182..d9607d86 100644 --- a/README.adoc +++ b/README.adoc @@ -387,6 +387,17 @@ One is possible to plug in your own bespoke authentication provider by implement <> section explains how to pass custom authorization scheme and claim from GRPC client. +=== Obtaining Authentication details + +To obtain `Authentication` object in the implementation of *secured method*, please use below snippet + +[source,java] +---- +final Authentication auth = GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get(); +---- + + + === Client side configuration support By adding `io.github.lognet:grpc-client-spring-boot-starter` dependency to your *java grpc client* application you can easily configure per-channel or per-call credentials : diff --git a/ReleaseNotes.adoc b/ReleaseNotes.adoc index 2f56f908..a4cbde0a 100644 --- a/ReleaseNotes.adoc +++ b/ReleaseNotes.adoc @@ -1,8 +1,15 @@ +== Version 4.1.0 +* gRPC version upgraded to 1.31.2 +* Fixed the issue with obtaining `Authentication` details in secured object implementation. +* Fixed the issue with providing client-side user credentials. + == Version 4.0.0 * Spring Security framework integration * gRPC version upgraded to 1.32.1 * Spring Boot 2.3.3 +[IMPORTANT] +Please use `4.1.0` version, `4.0.0` has issue with obtaining Authentication details in secured object implementation. == Version 3.5.7 * gRPC version upgraded to 1.31.1 diff --git a/build.gradle b/build.gradle index 08ee04f3..a7f40781 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { springBoot_2_X_Version = '2.3.3.RELEASE' - grpcVersion = '1.32.1' + grpcVersion = '1.32.2' } repositories { mavenCentral() diff --git a/gradle.properties b/gradle.properties index 24f965ba..557c4432 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.1-SNAPSHOT +version=4.1.0 group=io.github.lognet description=Spring Boot starter for Google RPC. gitHubUrl=https\://github.com/LogNet/grpc-spring-boot-starter diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java index c2b17f51..2e5e2fbb 100644 --- a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/GreeterService.java @@ -6,9 +6,9 @@ import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.lognet.springboot.grpc.GRpcService; +import org.lognet.springboot.grpc.security.GrpcSecurity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @Slf4j @@ -28,7 +28,7 @@ public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver responseObserver) { - final Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + final Authentication auth = GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get(); String user = auth.getName(); if(auth instanceof JwtAuthenticationToken){ user = JwtAuthenticationToken.class.cast(auth).getTokenAttributes().get("preferred_username").toString(); diff --git a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/SecuredGreeterService.java b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/SecuredGreeterService.java index f24d3daf..47bf37f4 100644 --- a/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/SecuredGreeterService.java +++ b/grpc-spring-boot-starter-demo/src/main/java/org/lognet/springboot/grpc/demo/SecuredGreeterService.java @@ -6,9 +6,9 @@ import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.lognet.springboot.grpc.GRpcService; +import org.lognet.springboot.grpc.security.GrpcSecurity; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @Slf4j @@ -18,7 +18,7 @@ public class SecuredGreeterService extends SecuredGreeterGrpc.SecuredGreeterImpl @Override public void sayAuthHello(Empty request, StreamObserver responseObserver) { - final Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + final Authentication auth = GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get(); String user = auth.getName(); if(auth instanceof JwtAuthenticationToken){ user = JwtAuthenticationToken.class.cast(auth).getTokenAttributes().get("preferred_username").toString(); diff --git a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/CalculatorGrpc.java b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/CalculatorGrpc.java index 61521b51..b6e7a0dc 100644 --- a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/CalculatorGrpc.java +++ b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/CalculatorGrpc.java @@ -10,7 +10,7 @@ /** */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.32.1)", + value = "by gRPC proto compiler (version 1.32.2)", comments = "Source: calculator.proto") public final class CalculatorGrpc { diff --git a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/GreeterGrpc.java b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/GreeterGrpc.java index 8cd57928..2b70dcd8 100644 --- a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/GreeterGrpc.java +++ b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/GreeterGrpc.java @@ -13,7 +13,7 @@ * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.32.1)", + value = "by gRPC proto compiler (version 1.32.2)", comments = "Source: greeter.proto") public final class GreeterGrpc { diff --git a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/SecuredGreeterGrpc.java b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/SecuredGreeterGrpc.java index 08773e74..a4841f82 100644 --- a/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/SecuredGreeterGrpc.java +++ b/grpc-spring-boot-starter-demo/src/main/protoGen/io/grpc/examples/SecuredGreeterGrpc.java @@ -10,7 +10,7 @@ /** */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.32.1)", + value = "by gRPC proto compiler (version 1.32.2)", comments = "Source: greeter.proto") public final class SecuredGreeterGrpc { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java new file mode 100644 index 00000000..61922a5c --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java @@ -0,0 +1,168 @@ +package org.lognet.springboot.grpc.auth; + +import com.google.protobuf.Empty; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.GreeterGrpc.GreeterFutureStub; +import io.grpc.examples.SecuredGreeterGrpc; +import lombok.extern.slf4j.Slf4j; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.lognet.springboot.grpc.GrpcServerTestBase; +import org.lognet.springboot.grpc.demo.DemoApp; +import org.lognet.springboot.grpc.security.AuthCallCredentials; +import org.lognet.springboot.grpc.security.AuthHeader; +import org.lognet.springboot.grpc.security.EnableGrpcSecurity; +import org.lognet.springboot.grpc.security.GrpcSecurity; +import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(classes = DemoApp.class, properties = "spring.cloud.service-registry.auto-registration.enabled=false") +@RunWith(SpringRunner.class) +@Import({ConcurrentAuthConfigTest.TestCfg.class}) +@Slf4j +public class ConcurrentAuthConfigTest extends GrpcServerTestBase { + + private static User user1 = new User("test1", "test1", Collections.EMPTY_LIST); + private static User user2 = new User("test2", "test2", Collections.EMPTY_LIST); + + private AuthCallCredentials user1CallCredentials = new AuthCallCredentials( + AuthHeader.builder().basic(user1.getUsername(), user1.getPassword().getBytes())); + + private AuthCallCredentials user2CallCredentials = new AuthCallCredentials( + AuthHeader.builder().basic(user2.getUsername(), user2.getPassword().getBytes())); + + @TestConfiguration + static class TestCfg { + + @EnableGrpcSecurity + public class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter { + + @Override + public void configure(GrpcSecurity builder) throws Exception { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + UserDetailsService users = new InMemoryUserDetailsManager(user1, user2); + provider.setUserDetailsService(users); + provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); + + builder + .authenticationProvider(provider) + .authorizeRequests() + .anyMethod().authenticated(); + } + + } + } + + @Test + public void concurrentTest() throws InterruptedException { + System.out.println(); + + final SecuredGreeterGrpc.SecuredGreeterBlockingStub unsecuredFutureStub = SecuredGreeterGrpc + .newBlockingStub(selectedChanel); + + final SecuredGreeterGrpc.SecuredGreeterBlockingStub securedFutureStub1 = unsecuredFutureStub + .withCallCredentials(user1CallCredentials); + + final SecuredGreeterGrpc.SecuredGreeterBlockingStub securedFutureStub2 = unsecuredFutureStub + .withCallCredentials(user2CallCredentials); + + + int parallelTests = 10; + + List threads = new ArrayList<>(); + // Number of threads that passed the test + AtomicInteger successCounter = new AtomicInteger(0); + AtomicInteger failureCounter = new AtomicInteger(0); + + Function authenticated = i -> { + SecuredGreeterGrpc.SecuredGreeterBlockingStub stub = null; + User user = null; + if (0 == i % 2) { + stub = securedFutureStub1; + user = user1; + }else{ + stub = securedFutureStub2; + user = user2; + } + final String reply = stub.sayAuthHello(Empty.getDefaultInstance()).getMessage(); + assertThat(reply, Matchers.containsString(user.getUsername())); + return null; + }; + Runnable unauthenticated = () -> { + StatusRuntimeException err = assertThrows(StatusRuntimeException.class, + () -> unsecuredFutureStub.sayAuthHello(Empty.getDefaultInstance()).getMessage()); + assertEquals(Status.Code.UNAUTHENTICATED, err.getStatus().getCode()); + }; + + // Check that the assertions work as is (single threaded) + authenticated.apply(0); + unauthenticated.run(); + + for (int i = 0; i < parallelTests; i++) { + Thread success = new Thread(() -> { + + for (int j = 0; j < 1000; j++) { + authenticated.apply(j); + } + successCounter.incrementAndGet(); + log.info("All passed"); + }); + success.setUncaughtExceptionHandler((thread, ex) -> { + log.error("SECURITY ???", ex); + }); + threads.add(success); + + Thread failure = new Thread(() -> { + + for (int j = 0; j < 1000; j++) { + unauthenticated.run(); + } + failureCounter.incrementAndGet(); + log.info("All passed"); + }); + failure.setUncaughtExceptionHandler((thread, ex) -> { + log.error("SECURITY BYPASSED", ex); + }); + + threads.add(failure); + } + + Collections.shuffle(threads); + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + + assertAll(() -> assertEquals(parallelTests, successCounter.get()), + () -> assertEquals(parallelTests, failureCounter.get())); + } + + @Override + protected GreeterFutureStub beforeGreeting(GreeterFutureStub stub) { + return stub.withCallCredentials(user1CallCredentials); + } + +} \ No newline at end of file diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java index 362b0c80..11e9d3ef 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java @@ -44,9 +44,6 @@ public class JwtRoleTest extends JwtAuthBaseTest { - - - @TestConfiguration static class TestCfg { @@ -82,18 +79,18 @@ public void concurrencyTest() throws InterruptedException, ExecutionException { final CyclicBarrier barrier = new CyclicBarrier(concurrency); final CountDownLatch endCountDownLatch = new CountDownLatch(concurrency); - AtomicInteger shouldSucceed = new AtomicInteger(); - AtomicInteger shouldFail = new AtomicInteger(); + AtomicInteger shouldSucceed = new AtomicInteger(); + AtomicInteger shouldFail = new AtomicInteger(); - final List> result = Stream.iterate(0, i -> i + 1) + final List> result = Stream.iterate(0, i -> i + 1) .limit(concurrency) .map(i -> new Callable() { @Override public Boolean call() throws Exception { - System.out.println("About to start call "+i); + System.out.println("About to start call " + i); barrier.await(); - System.out.println("Start call "+i); + System.out.println("Start call " + i); try { if (i % 2 == 0) { shouldSucceed.incrementAndGet(); @@ -109,8 +106,8 @@ public Boolean call() throws Exception { return true; } catch (Exception e) { return false; - }finally { - System.out.println("Call "+i+" finished"); + } finally { + System.out.println("Call " + i + " finished"); endCountDownLatch.countDown(); } } @@ -120,21 +117,16 @@ public Boolean call() throws Exception { endCountDownLatch.await(); - int failed=0, succeeded=0; - for(Future res: result ){ - if(res.get()){ + int failed = 0, succeeded = 0; + for (Future res : result) { + if (res.get()) { ++succeeded; - }else { + } else { ++failed; } } - assertThat(succeeded,Matchers.is(shouldSucceed.get())); - assertThat(failed,Matchers.is(shouldFail.get())); - - - - - + assertThat(succeeded, Matchers.is(shouldSucceed.get())); + assertThat(failed, Matchers.is(shouldFail.get())); } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java index 81d1c492..4302e905 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java @@ -1,5 +1,6 @@ package org.lognet.springboot.grpc.security; +import io.grpc.Context; import io.grpc.ServerInterceptor; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; @@ -12,6 +13,7 @@ import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.Arrays; @@ -21,6 +23,7 @@ public class GrpcSecurity extends AbstractConfiguredSecurityBuilder AUTHENTICATION_CONTEXT_KEY = Context.key("AUTHENTICATION"); public GrpcSecurity(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index 59874155..5a9eb7a4 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -1,5 +1,7 @@ package org.lognet.springboot.grpc.security; +import io.grpc.Context; +import io.grpc.Contexts; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.ServerCall; @@ -9,71 +11,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; -import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Optional; @Slf4j public class SecurityInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor, Ordered { - class SecurityServerCallListener extends ServerCall.Listener { - private InterceptorStatusToken token = null; - private Optional> listener; - - protected SecurityServerCallListener(ServerCall call, - Metadata headers, - ServerCallHandler next) { - try { - token = SecurityInterceptor.this.beforeInvocation(call.getMethodDescriptor()); - listener = Optional.of(next.startCall(call, headers)); - } catch (Exception e) { - call.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()), new Metadata()); - listener = Optional.empty(); - } - - } - - @Override - public void onHalfClose() { - listener.ifPresent(ServerCall.Listener::onHalfClose); - tearDown(); - } - - @Override - public void onCancel() { - - listener.ifPresent(ServerCall.Listener::onCancel); - tearDown(); - } - - @Override - public void onComplete() { - listener.ifPresent(ServerCall.Listener::onComplete); - tearDown(); - } - - @Override - public void onMessage(ReqT message) { - listener.ifPresent(l -> l.onMessage(message)); - - } - - @Override - public void onReady() { - listener.ifPresent(ServerCall.Listener::onReady); - } - - private void tearDown() { - SecurityInterceptor.this.finallyInvocation(token); - SecurityInterceptor.this.afterInvocation(token, null); - } - } - private GrpcSecurityMetadataSource securedMethods; private AuthenticationSchemeSelector schemeSelector; @@ -104,6 +51,8 @@ public ServerCall.Listener interceptCall( Metadata headers, ServerCallHandler next) { + + final byte[] authorization = headers.get(Metadata.Key.of("Authorization"+Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER)); @@ -112,12 +61,31 @@ public ServerCall.Listener interceptCall( .orElseThrow(()->new RuntimeException("Can't get authentication from authorization header")); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authentication); - SecurityContextHolder.setContext(context); - return new SecurityServerCallListener<>(call, headers, next); + + try { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + beforeInvocation(call.getMethodDescriptor()); + + Context ctx = Context.current() + .withValue(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY, SecurityContextHolder.getContext().getAuthentication()); + + return Contexts.interceptCall(ctx,call,headers,next); + } catch (Exception e) { + + + call.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()), new Metadata()); + return new ServerCall.Listener() { + // noop + }; + }finally { + SecurityContextHolder.getContext().setAuthentication(null); + } + }