At Revinate, we’ve recently adopted gRPC as the preferred communication mechanism between our microservices. Based on Protocol Buffers and HTTP/2, gRPC offers a number of advantages over our previous approaches to microservice communication. The contract-first approach of defining Protocol Buffer messages and gRPC services makes it easy to write tests and verify service behavior. The bidirectional streaming features and asynchronous programming style enabled by HTTP/2 lead to more reactive services. Protocol Buffer’s code-generation capabilities, with support for multiple languages, allow us to rapidly adopt gRPC across all of our microservices, which are variously written in Java, Scala, Go, Ruby, and PHP.

Our first gRPC services are written in Java and are built on Spring Boot. Thanks to the gRPC Spring Boot Starter module, it takes very little configuration to integrate a gRPC service endpoint implementation with a Spring Boot-based server. Once we had the services up and running, we wanted to secure the services and enable our existing authentication schemes. Our authentication schemes are fairly common for implementors of microservices: each of our services need to support HTTP Basic Authentication as well as OAuth2 authentication with JSON Web Tokens.

However, after reading the gRPC documentation, we were disappointed to find that these authentication schemes are not supported out-of-the-box. Besides support for SSL/TLS credentials, the only authentication mechanism supplied with gRPC is token-based authentication for use with Google services only. Fortunately, the gRPC Java runtime comes with all the building blocks needed to implement our own authentication mechanisms, we just need to build the missing pieces ourselves.

Choosing a security framework

Since we are building a Spring Boot application, it may seem natural to turn to Spring Security to secure the gRPC services. However, there are both pros and cons to this approach.

Using the gRPC Java runtime, each gRPC service method corresponds to a Java method in our service endpoint implementation class. To apply granular authorization requirements to each gRPC service method, we can leverage Spring Security’s method-based security mechanism. This allows us to succinctly express authorization requirements directly in our gRPC service endpoint implementation. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GRpcService
public class DemoGrpcService extends DemoServiceGrpc.DemoServiceImplBase {

    @Override
    @PreAuthorize("hasRole('USER')")
    public void list(ListRequest request, StreamObserver<ListResponse> responseObserver) {
        ...
    }

    @Override
    @PreAuthorize("hasRole('ADMIN')")
    public void buy(BuyRequest request, StreamObserver<BuyResponse> responseObserver) {
        ...
    }
}

Behind the scenes, Spring Security uses a number of components to make method-based security work. In a normal web application, the components that do most of the heavy lifting are servlet filters. However, the gRPC Java runtime is based on netty, not servlets. Thus, none of the functionality provided by Spring Security servlet filters will be invoked when clients make requests to gRPC endpoints. We also can’t reuse any of the Spring Security servlet filters because they are tied to servlet request and response APIs. To leverage Spring Security, we must re-implement some of the functionality provided by these servlet filters. Fortunately, the gRPC runtime supports the use of interceptors, which are functionally very similar to servlet filters. The gRPC Spring Boot Starter module also supports annotation-driven registration of gRPC interceptors. For our Java gRPC services, we decided to translate the necessary Spring Security servlet filters into gRPC interceptors in order to leverage Spring Security method-based authorization.

Implementing security interceptors

In this section, we’ll walk through each of the gRPC interceptors we implemented to secure our gRPC services with Spring Security. To save space, only key sections of each interceptor class' source code are shown here. The full source code for all the interceptor classes can be found in our grpc-spring-security-demo project hosted on Github. This project is a fully-functional Spring Boot application with gRPC services secured with Spring Security. Within this project, the following sections may be of particular interest:

gRPC interceptors are very similar to servlet filters, with a couple of notable differences. A chain of servlet filters, known as a filter chain, can be visualized as a call stack. Suppose filter A is the first filter in the chain. In filter A’s doFilter method, we can do some work on the request, then invoke doFilter on the filterChain argument passed into filter A’s doFilter method. This pushes the next filter after filter A on the stack, which then places the next filter on the stack, and so on, until the servlet is placed on the stack. Eventually the servlet and all the other filters are popped off the stack, until control returns to filter A’s doFilter method. At this point filter A can do more work, for example to process the response. This design means that the first filter in the chain also gets to perform the very last action in the handling of any particular request.

Similar to the servlet filter’s doFilter method, gRPC interceptors implement the interceptCall method. However, gRPC takes a non-blocking approach to chaining together interceptors. Instead of invoking the rest of the interceptor chain explicitly, the interceptCall method instead returns a ServerCall.Listener object to encapsulate the next step in the request processing chain. Since the next processing step is provided to the runtime as the return value of interceptCall, interceptCall never gets control back. Therefore, to perform any actions after other interceptors and the service implementation have completed, we must return a custom ServerCall.Listener from interceptCall, one that delegates to the original ServerCall.Listener passed into interceptCall, but adds additional behavior after the original listener completes. gRPC provides the SimpleForwardingServerCallListener for this purpose. Two of the interceptors below demonstrate this approach. As with servlet filters, if we want an interceptor to perform the last action in the chain, we must place that interceptor first.

The following interceptors are presented in the order that they appear in the interceptor chain. Conveniently, we can use Spring’s @Order annotation to determine the absolute placement of our interceptors. The interceptors are registered as Spring Beans via the @GRpcGlobalInterceptor Spring Component annotation.

Security context persistence interceptor

In a Spring Security filter chain, the SecurityContextPersistenceFilter must come first, since it sets up the SecurityContext used by the rest of the framework. It is also the first interceptor we’ll implement. Our security context persistence interceptor won’t be as general as Spring Security’s SecurityContextPersistenceFilter. In particular, we won’t support loading and storing the security context into persistent storage, and we will only support the ThreadLocalSecurityContextHolderStrategy, which stores the security context in a ThreadLocal object. Given these constraints, there’s actually only one thing our interceptor needs to do: clear out the ThreadLocal security context storage at the end of request processing, so that the thread may be reused by another request. This is the very last thing that should happen during request processing, hence the placement of this interceptor at the beginning of the chain.

Using ThreadLocal to store the security context is less than ideal given gRPC’s nonblocking design. At the end of this blog post we’ll discuss possible improvements to this implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@GRpcGlobalInterceptor
@Order(10)
public class SecurityContextPersistenceInterceptor implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        ServerCall.Listener<ReqT> delegate = next.startCall(call, headers);
        return new SimpleForwardingServerCallListener<ReqT>(delegate) {
            @Override
            public void onComplete() {
                try {
                    super.onComplete();
                } finally {
                    SecurityContextHolder.clearContext();
                }
            }
        };
    }
}

This interceptor shows the SimpleForwardingServerCallListener in action. We wrap the onComplete handler of the original listener, and add an action to clear the security context at the end of the handler.

Exception translation interceptor

Using Spring Security’s method-based security, any authorization issues at the service method level result in security exceptions being thrown. Spring Security’s ExceptionTranslationFilter is responsible for translating these exceptions into HTTP responses with the appropriate status codes and message bodies. We need the same functionality in our gRPC interceptor chain. Our implementation is nearly a direct translation of the Spring Security filter.

Here we also need the SimpleForwardingServerCallListener, but instead of wrapping the onComplete handler, we wrap the onHalfClose handler instead. onHalfClose is where all the processing done by the service implementation occurs, so we need to catch any security-related exceptions thrown by the delegate’s handler and translate them to appropriate responses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@GRpcGlobalInterceptor
@Order(20)
public class ExceptionTranslationInterceptor implements ServerInterceptor {

    private ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
    private AuthenticationTrustResolver authenticationTrustResolver =
        new AuthenticationTrustResolverImpl();

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        ServerCall.Listener<ReqT> delegate = next.startCall(call, headers);
        return new SimpleForwardingServerCallListener<ReqT>(delegate) {
            @Override
            public void onHalfClose() {
                try {
                    super.onHalfClose();
                } catch (Exception e) {
                    Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
                    AuthenticationException authenticationException =
                        (AuthenticationException) throwableAnalyzer
                        .getFirstThrowableOfType(AuthenticationException.class, causeChain);

                    if (Objects.nonNull(authenticationException)) {
                        handleAuthenticationException(authenticationException);
                    } else {
                        AccessDeniedException accessDeniedException =
                            (AccessDeniedException) throwableAnalyzer
                            .getFirstThrowableOfType(AccessDeniedException.class, causeChain);

                        if (Objects.nonNull(accessDeniedException)) {
                            handleAccessDeniedException(accessDeniedException);
                        } else {
                            throw e;
                        }
                    }
                }
            }

            private void handleAuthenticationException(AuthenticationException exception) {
                call.close(Status.UNAUTHENTICATED.withDescription(exception.getMessage())
                        .withCause(exception), new Metadata());
            }

            private void handleAccessDeniedException(AccessDeniedException exception) {
                Authentication authentication = SecurityContextHolder
                    .getContext().getAuthentication();

                if (authenticationTrustResolver.isAnonymous(authentication)) {
                    call.close(Status.UNAUTHENTICATED
                        .withDescription("Authentication is required to access this resource")
                        .withCause(exception), new Metadata());
                } else {
                    call.close(Status.PERMISSION_DENIED.withDescription(exception.getMessage())
                            .withCause(exception), new Metadata());
                }
            }
        };
    }
}

Basic authentication interceptor

With the framework requirements out of the way, we can now implement our first authentication scheme, HTTP Basic Authentication. Writing this filter is very simple, since we can model it on Spring Security’s BasicAuthenticationFilter. In the code below, the interceptCall method follows much of the same flow as BasicAuthenticationFilter#doFilterInternal, and the authenticationIsRequired method is nearly identical to the method of the same name from BasicAuthenticationFilter.

When an authentication exception occurs in this interceptor, we can simply throw a StatusRuntimeException. The gRPC runtime takes care of translating it into an appropriate response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@GRpcGlobalInterceptor
@Order(50)
public class BasicAuthenticationInterceptor implements ServerInterceptor {

    private final AuthenticationManager authenticationManager;

    @Autowired
    public BasicAuthenticationInterceptor(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        String authHeader = nullToEmpty(headers.get(Metadata.Key.of("Authorization",
            Metadata.ASCII_STRING_MARSHALLER)));
        if (!authHeader.startsWith("Basic ")) {
            return next.startCall(call, headers);
        }

        try {
            String[] tokens = decodeBasicAuth(authHeader);
            String username = tokens[0];

            if (authenticationIsRequired(username)) {
                Authentication authRequest =
                    new UsernamePasswordAuthenticationToken(username, tokens[1]);
                Authentication authResult = authenticationManager.authenticate(authRequest);
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
        } catch (AuthenticationException e) {
            SecurityContextHolder.clearContext();
            throw Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e)
                .asRuntimeException();
        }

        return next.startCall(call, headers);
    }

    private String[] decodeBasicAuth(String authHeader) {
        String basicAuth;
        try {
            basicAuth = new String(
                Base64.getDecoder()
                    .decode(authHeader.substring(6).getBytes(StandardCharsets.UTF_8)),
                StandardCharsets.UTF_8);
        } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }

        int delim = basicAuth.indexOf(":");
        if (delim == -1) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }

        return new String[] { basicAuth.substring(0, delim), basicAuth.substring(delim + 1) };
    }

    private boolean authenticationIsRequired(String username) {
        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        if (Objects.isNull(existingAuth) || !existingAuth.isAuthenticated()) {
            return true;
        }

        if (existingAuth instanceof UsernamePasswordAuthenticationToken
                && !existingAuth.getName().equals(username)) {
            return true;
        }

        if (existingAuth instanceof AnonymousAuthenticationToken) {
            return true;
        }

        return false;
    }
}

OAuth2 authentication interceptor

For our simple use case of only supporting OAuth2 authentication with JSON Web Tokens, there isn’t a Spring Security filter that gives us the exact flow. Fortunately, Spring Security OAuth2 combined with Spring Boot gives us an implementation of ResourceServerTokenServices that can handle all the heavy lifting of extracting and validating OAuth2 credentials from JSON Web Tokens. All we have to do is inject this bean into our interceptor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@GRpcGlobalInterceptor
@Order(60)
public class Oauth2AuthenticationInterceptor implements ServerInterceptor {

    private final ResourceServerTokenServices tokenServices;

    @Autowired
    public Oauth2AuthenticationInterceptor(ResourceServerTokenServices tokenServices) {
        this.tokenServices = tokenServices;
    }

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        String authHeader = nullToEmpty(headers.get(Metadata.Key.of("Authorization",
            Metadata.ASCII_STRING_MARSHALLER)));
        if (!(authHeader.startsWith("Bearer ") || authHeader.startsWith("bearer "))) {
            return next.startCall(call, headers);
        }

        try {
            String token = authHeader.substring(7);

            if (authenticationIsRequired()) {
                Authentication authResult = tokenServices.loadAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
        } catch (AuthenticationException | OAuth2Exception e) {
            SecurityContextHolder.clearContext();
            throw Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e)
                .asRuntimeException();
        }

        return next.startCall(call, headers);
    }

    private boolean authenticationIsRequired() {
        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        if (Objects.isNull(existingAuth) || !existingAuth.isAuthenticated()) {
            return true;
        }

        if (existingAuth instanceof AnonymousAuthenticationToken) {
            return true;
        }

        return false;
    }
}

Anonymous authentication interceptor

Spring Security authorization components expect the SecurityContext to be populated with an authentication object, and will throw an unexpected exception if that object is null. Thus, if all of the authentication interceptors in the chain were bypassed, we need to populate the SecurityContext with an anonymous authentication. This is the last interceptor in the chain as well as the last interceptor we need to implement. It is based on Spring Security’s AnonymousAuthenticationFilter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GRpcGlobalInterceptor
@Order(100)
public class AnonymousAuthenticationInterceptor implements ServerInterceptor {

    private String key = UUID.randomUUID().toString();

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        if (Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
            SecurityContextHolder.getContext().setAuthentication(
                new AnonymousAuthenticationToken(key, "anonymousUser",
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))));
        }

        return next.startCall(call, headers);
    }
}

Conclusion

After implementing all the interceptors, the tradeoffs for using Spring Security to secure gRPC services becomes clear. We had to write quite a bit of framework-specific code to tie into Spring Security nicely. On the other hand, we were able to leverage a number of Spring Security components, such as the configuration-based AuthenticationManager, the OAuth2 token validator, and the AOP-based method-level security interceptors. These Spring Security components would’ve been much more difficult to write in a secure way than the gRPC interceptors we wrote to leverage Spring Security. We can therefore conclude that our security implementation was greatly simplified with the use of Spring Security.

Of the five interceptors we wrote, only two implemented some kind of authentication mechanism. We only implemented these two because they are the only mechanisms our organization needed. Adding support for additional authentication mechanisms is easy with this approach. Simply implement the interceptor, register it with Spring using the @GRpcGlobalInterceptor annotation, and place it at a specific location within the interceptor chain using the @Order annotaton.

As mentioned earlier, our design uses ThreadLocal to store the Spring SecurityContext. While this works great for Servlet-based environments, it is not optimal for gRPC’s netty-based nonblocking runtime. Spring Security allows us to specify an alternative SecurityContext store by implementing a custom SecurityContextHolderStrategy. Additionally, the gRPC Java runtime provides the Context class, which can be used to carry state across API boundaries and between threads. One of the main usages of gRPC Contexts is the propagation of security credentials. We can therefore implement a SecurityContextHolderStrategy that uses gRPC Contexts to store the SecurityContext. In a future blog post we may discuss an implementation of this idea.

Tell us what you think of our approach to securing gRPC services! Leave a comment below, or submit a ticket or pull request to our Github repository.