GRPC Call Credentials in Java

I have been using GRPC in production for some time now. I have worked for a range of clients who have non-standard authentication and authorisation systems. GRPC offers several methods of authentication out of the box:

Authentication in GRPC is implemented using credentials. Credentials can be attached to:

Most of the systems I have worked on use SSL/TLS on the Channel to secure the communications. Call Credentials are generally used to authenticate specific users, or for inter-service user credential transmission.

I have not found any good guides on how to setup and use Call Credentials in Java, so here we are. This guide will attempt to clarify how one might use a token based system with GRPC. All the code for this guide can be found in the Github repository grpc-call-credentials.

WARNING At the time of writing the implementation of Call Credentials in the Java API is unstable and is subject to change.

The first place to start is our implementation of the AuthorisationService the Java class that is responsible for generating tokens for users and for verifying said tokens. This implementation is a dummy implementation. It is far from cryptographically secure, it merely base64 encodes an embedded secret along with the username for the user. For verification it base64 decodes the token, verifying the secret is as expected, then providing the username.

public class AuthenticationService {

    private String secret;

    public AuthenticationService(String secret) {
        this.secret = secret;
    }

    /**
     * Base64 encode username and secret
     */
    public String generateToken(String username) {
        return Base64
            .getEncoder()
            .encodeToString((secret + username)
            .getBytes());
    }

    /**
     * Base64 decode the token
     * Ensure the secret is as expected
     * Return the embedded username
     */
    public String validateToken(String token)
            throws AuthenticationException {
        
        String decodedToken = new String(Base64
            .getDecoder()
            .decode(token));
        if (!decodedToken.startsWith(secret)) {
            throw new AuthenticationException("Invalid token");
        } else {
            return decodedToken.substring(secret.length());
        }
    }
}

The token generated by the AuthenticationService can then be used by the AuthenticationCallCredentials.

public class AuthenticationCallCredentials extends CallCredentials {

    private String token;

    public AuthenticationCallCredentials(String token) {
        this.token = token;
    }

    @Override
    public void applyRequestMetadata(
            RequestInfo requestInfo,
            Executor executor,
            MetadataApplier metadataApplier) {
        executor.execute(() -> {
            try {
                Metadata headers = new Metadata();
                headers.put(AuthenticationConstants.META_DATA_KEY, "Bearer " + token);
                metadataApplier.apply(headers);
            } catch (Throwable e) {
                metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));
            }
        });

    }

    @Override
    public void thisUsesUnstableApi() {
        // yes this is unstable :(
    }
}

This class is used in the GRPC client to add headers to the metadata that is sent with each RPC. The Javadoc states that this method should not block. This is why an Executor is provided, although this example doesn’t really need it. The RPC proceeds only once the MetadataApplier has been called.

This class uses the AuthenticationConstants.META_DATA_KEY to add the token to the metadata for the RPC. This definition of this constant is detailed below. An Metadata.ASCII_STRING_MARSHALLER is used as the token to be sent is a simple string. There are other Marshallers that can be used depending on what data you are trying to send.

Metadata.Key<String> META_DATA_KEY =
    Metadata.Key.of("Authentication", Metadata.ASCII_STRING_MARSHALLER);

This AuthenticationCallCredentials can then be used on a GRPC stub on a call-by-call basis. The below code uses the example Greeter service that is used in the Github repository.

GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc
    .newBlockingStub(channel);

String token = authenticationService.generateToken("alice");
GreetRequest request = GreetRequest
    .newBuilder()
    .setName("alice")
    .build();

GreetResponse firstGreetResponse = stub
    .withCallCredentials(new AuthenticationCallCredentials(token))
    .greet(request);

It is as simple as adding the AuthenticationCallCredentials to the stub on each RPC using the .withCallCredentials method.

This concludes the code for adding the token to an RPC using metadata. Next we can move on to how to implement the authentication on the server.

On the server a ServerInterceptor is used to extract the token from the RPC metadata. The ServerInterceptor is a simple interface that defines one method:

public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> serverCall,
            Metadata metadata,
            ServerCallHandler<ReqT, RespT> serverCallHandler) {

In this method we can perform our user authentication. The ServerCall can be used to respond back to the client, for example in the case a user is not authenticated we can use it to send the UNAUTHENTED Status.

The Metadata contains the headers sent from the client. This is what we will extract the token from.

The ServerCallHandler is used to allow the RPC to proceed.

This is our implementation of this method.

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
        ServerCall<ReqT, RespT> serverCall,
        Metadata metadata,
        ServerCallHandler<ReqT, RespT> serverCallHandler) {
    String header = metadata.get(AuthenticationConstants.META_DATA_KEY);

    if (Strings.isNullOrEmpty(header)) {
        serverCall.close(Status.UNAUTHENTICATED.withDescription("No authentication header"), metadata);
    } else if (!header.startsWith("Bearer ")) {
        serverCall.close(Status.UNAUTHENTICATED.withDescription("Non bearer token provided"), metadata);
    } else {
        String token = header.substring("Bearer ".length());
        try {
            String username = authenticationService.validateToken(token);
            Context context = Context
                .current()
                .withValue(AuthenticationConstants.CONTEXT_USERNAME_KEY, username);
            return Contexts.interceptCall(
                context,
                serverCall,
                metadata,
                serverCallHandler);
        } catch (AuthenticationException e) {
            serverCall
                .close(Status.UNAUTHENTICATED
                .withDescription("Rejected by Authentication Service"),
                metadata);
        }
    }
    return new ServerCall.Listener<ReqT>() {};
}

As you can see this method:

The final step adds a new entry to the current Context using a Context.Key. The Context.Key we use is defined as:

Context.Key<String> CONTEXT_USERNAME_KEY = Context.key("username");

The Contexts.interceptCall is a helper method to augment the Context before calling the ServerCallHandler.

This AuthenticationInterceptor can be added to the server as follows:

Server server = NettyServerBuilder
    .forPort(8080)
    .intercept(new AuthenticationInterceptor(authenticationService))
    .addService(new GreeterServer())
    .build();

The final part of the puzzle is accessing the username from the context as part of the RPC service implementation. For this we use the Context.Key we previously defined:

String username = AuthenticationConstants.CONTEXT_USERNAME_KEY.get();

This concludes the guide on how to implement custom call-based authentication in Java for GRPC. I hope you found this useful.

Comments

comments powered by Disqus