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:
- SSL/TLS
- ALTS
- Google based Token Authentication
Authentication in GRPC is implemented using credentials. Credentials can be attached to:
- Channels - which are attached to a Channel
- Call Credentials - which are attached to a Call
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:
- Extracts the token from the
Metadata
headers, using the sameMetadata.Key
that was used to add it in the client. - Checks if the header is present, if it is not we can respond with a
Status.UNAUTHENTICATED
. - Check if the authentication header is of a
Bearer
type, again responding withStatus.UNAUTHENTICATED
if it is not. - Validates the token with our
AuthenticationService
, if it is not valid and anAuthenticationException
is thrown we respond with aStatus.UNAUTHENTICATED
. - Finally, we add the username to the
Context
so it can be used in the RPC service.
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.