Skip to content

Commit 52f4ff7

Browse files
author
Alexander Furer
committed
fixes #178
1 parent b93c176 commit 52f4ff7

File tree

7 files changed

+65
-19
lines changed

7 files changed

+65
-19
lines changed

README.adoc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,8 @@ class MyClient{
532532
final AuthClientInterceptor clientInterceptor = new AuthClientInterceptor(<1>
533533
AuthHeader.builder()
534534
.bearer()
535-
.tokenSupplier(this::generateToken)<3>
535+
.binaryFormat(true)<3>
536+
.tokenSupplier(this::generateToken)<4>
536537
);
537538
538539
Channel authenticatedChannel = ClientInterceptors.intercept(
@@ -541,14 +542,18 @@ class MyClient{
541542
// use authenticatedChannel to invoke GRPC service
542543
}
543544
544-
private ByteBuffer generateToken(){ <3>
545+
private ByteBuffer generateToken(){ <4>
545546
// generate bearer token against your resource server
546547
}
547548
}
548549
----
549550
<1> Create client interceptor
550551
<2> Intercept channel
551-
<3> Provide token generator function (Please refer to link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java[for example].)
552+
<3> Turn the binary format on/off: +
553+
* When `true`, the authentication header is sent with `Authentication-bin` key using https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.BinaryMarshaller.html[binary marshaller].
554+
* When `false`, the authentication header is sent with `Authentication` key using https://grpc.github.io/grpc-java/javadoc/io/grpc/Metadata.AsciiMarshaller.html[ASCII marshaller].
555+
556+
<4> Provide token generator function (Please refer to link:grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java[for example].)
552557

553558
Per-call::
554559
+

ReleaseNotes.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
== Version 4.4.3-SNAPSHOT
2+
* Fixes *178
3+
* gRPC response status set to `PERMISSION_DENIED` when user has insufficient privileges to invoke gRPC method.
4+
15
== Version 4.4.2
26
* Spring Boot `2.4.1`
37
* Spring Cloud `2020.0.0`

grpc-client-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthHeader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
public class AuthHeader implements Constants {
1212
private final Supplier<ByteBuffer> tokenSupplier;
1313
private final String authScheme;
14+
@Builder.Default
15+
private final boolean binaryFormat = true;
1416

1517
public static class AuthHeaderBuilder {
1618

@@ -35,14 +37,19 @@ public AuthHeader.AuthHeaderBuilder basic(String userName, byte[] password) {
3537

3638

3739
}
40+
3841
public Metadata attach(Metadata metadataHeader){
3942
ByteBuffer token = tokenSupplier.get();
4043
final byte[] header = ByteBuffer.allocate(authScheme.length() + token.remaining() + 1)
4144
.put(authScheme.getBytes())
4245
.put((byte)' ')
4346
.put(token)
4447
.array();
45-
metadataHeader.put(Constants.AUTH_HEADER_KEY,header);
48+
if(binaryFormat) {
49+
metadataHeader.put(Constants.AUTH_HEADER_BIN_KEY, header);
50+
}else{
51+
metadataHeader.put(Constants.AUTH_HEADER_KEY, new String(header));
52+
}
4653
return metadataHeader;
4754
}
4855
}

grpc-client-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/Constants.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55

66
public interface Constants {
7-
Metadata.Key<byte[]> AUTH_HEADER_KEY = Metadata.Key.of("Authorization"+Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER);
7+
8+
Metadata.Key<String> AUTH_HEADER_KEY = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
9+
Metadata.Key<byte[]> AUTH_HEADER_BIN_KEY = Metadata.Key.of(AUTH_HEADER_KEY.name()+Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER);
810
String BEARER_AUTH_SCHEME="Bearer";
911
String BASIC_AUTH_SCHEME="Basic";
1012

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.lognet.springboot.grpc.auth;
22

33

4+
import io.grpc.Status;
45
import io.grpc.StatusRuntimeException;
56
import io.grpc.examples.CalculatorGrpc;
67
import io.grpc.examples.CalculatorOuterClass;
@@ -139,7 +140,7 @@ public void shouldFail() {
139140
.setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD)
140141
.build());
141142
});
142-
assertThat(statusRuntimeException.getMessage(), Matchers.containsString("UNAUTHENTICATED"));
143+
assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED));
143144

144145

145146
}

grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.lognet.springboot.grpc.auth;
22

33

4+
import com.google.protobuf.Empty;
45
import io.grpc.Channel;
56
import io.grpc.ClientInterceptors;
7+
import io.grpc.Status;
68
import io.grpc.StatusRuntimeException;
79
import io.grpc.examples.CalculatorGrpc;
810
import io.grpc.examples.CalculatorOuterClass;
@@ -27,15 +29,18 @@
2729
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
2830
import org.springframework.security.crypto.password.PasswordEncoder;
2931
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
30-
import org.springframework.test.context.ActiveProfiles;
3132
import org.springframework.test.context.junit4.SpringRunner;
3233

34+
import java.util.concurrent.ExecutionException;
35+
3336
import static org.hamcrest.MatcherAssert.assertThat;
37+
import static org.junit.Assert.assertNotNull;
3438
import static org.junit.Assert.assertThrows;
39+
import static org.junit.Assert.assertTrue;
3540

3641

3742
@SpringBootTest(classes = DemoApp.class)
38-
@ActiveProfiles("keycloack-test")
43+
//@ActiveProfiles("keycloack-test")
3944
@RunWith(SpringRunner.class)
4045
@Import({UserDetailsAuthTest.TestCfg.class})
4146
public class UserDetailsAuthTest extends GrpcServerTestBase {
@@ -70,6 +75,7 @@ public void configure(GrpcSecurity builder) throws Exception {
7075

7176
builder.authorizeRequests()
7277
.methods(GreeterGrpc.getSayHelloMethod()).hasAnyRole("reader")
78+
.methods(GreeterGrpc.getSayAuthOnlyHelloMethod()).hasAnyRole("reader")
7379
.methods(CalculatorGrpc.getCalculateMethod()).hasAnyRole("anotherRole")
7480
.and()
7581
.userDetailsService(new InMemoryUserDetailsManager(user()));
@@ -85,6 +91,19 @@ public void configure(GrpcSecurity builder) throws Exception {
8591
private UserDetails user;
8692

8793

94+
@Test
95+
public void simpleAuthHeaderFormat() throws ExecutionException, InterruptedException {
96+
97+
98+
99+
final GreeterGrpc.GreeterFutureStub greeterFutureStub = GreeterGrpc.newFutureStub(getChannel(false));
100+
final String reply = greeterFutureStub.sayAuthOnlyHello(Empty.newBuilder().build()).get().getMessage();
101+
assertNotNull("Reply should not be null",reply);
102+
assertTrue(String.format("Reply should contain name '%s'",user.getUsername()),reply.contains(user.getUsername()));
103+
104+
105+
}
106+
88107
@Test
89108
public void shouldFail() {
90109

@@ -95,17 +114,21 @@ public void shouldFail() {
95114
.setOperation(CalculatorOuterClass.CalculatorRequest.OperationType.ADD)
96115
.build());
97116
});
98-
assertThat(statusRuntimeException.getMessage(), Matchers.containsString("UNAUTHENTICATED"));
99-
117+
assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED));
100118

101119
}
102120

103121
@Override
104122
protected Channel getChannel() {
123+
return getChannel(true);
124+
}
125+
126+
protected Channel getChannel(boolean binaryFormat) {
105127

106128
final AuthClientInterceptor interceptor = new AuthClientInterceptor(AuthHeader.builder()
107129
.basic(user.getUsername(),TestCfg.DemoGrpcSecurityConfig.pwd.getBytes())
108-
);
130+
.binaryFormat(binaryFormat)
131+
);
109132
return ClientInterceptors.intercept(super.getChannel(), interceptor);
110133
}
111134

grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
import io.grpc.Status;
1111
import lombok.extern.slf4j.Slf4j;
1212
import org.springframework.core.Ordered;
13+
import org.springframework.security.access.AccessDeniedException;
1314
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
1415
import org.springframework.security.core.Authentication;
1516
import org.springframework.security.core.context.SecurityContext;
1617
import org.springframework.security.core.context.SecurityContextHolder;
1718

1819
import java.nio.ByteBuffer;
1920
import java.nio.charset.StandardCharsets;
21+
import java.util.Optional;
2022

2123
@Slf4j
2224
public class SecurityInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor, Ordered {
@@ -52,16 +54,15 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
5254
ServerCallHandler<ReqT, RespT> next) {
5355

5456

57+
final CharSequence authorization = Optional.ofNullable(headers.get(Metadata.Key.of("Authorization" + Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER)))
58+
.map(auth -> (CharSequence)StandardCharsets.UTF_8.decode(ByteBuffer.wrap(auth)))
59+
.orElse(headers.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER)));
5560

56-
final byte[] authorization = headers.get(Metadata.Key.of("Authorization"+Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER));
5761

5862

5963
final Authentication authentication = null==authorization?null:
60-
schemeSelector.getAuthScheme(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(authorization)))
61-
.orElseThrow(()->new RuntimeException("Can't get authentication from authorization header"));
62-
63-
64-
64+
schemeSelector.getAuthScheme(authorization)
65+
.orElseThrow(()->new RuntimeException("Can't get authentication from authorization header"));
6566

6667

6768
try {
@@ -75,9 +76,12 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
7576
.withValue(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY, SecurityContextHolder.getContext().getAuthentication());
7677

7778
return Contexts.interceptCall(ctx,call,headers,next);
79+
} catch (AccessDeniedException e) {
80+
call.close(Status.PERMISSION_DENIED.withDescription(e.getMessage()), new Metadata());
81+
return new ServerCall.Listener<ReqT>() {
82+
// noop
83+
};
7884
} catch (Exception e) {
79-
80-
8185
call.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()), new Metadata());
8286
return new ServerCall.Listener<ReqT>() {
8387
// noop

0 commit comments

Comments
 (0)