Image by Jason Costanza
Introduction to OAuth on gRPC
You might have heard of gRPC by now, a new RPC system that is quickly taking on JSON/HTTP as the recommended way to communicate between microservices. Its main selling points are well-defined schemas via Protocol Buffers 3, automatic client code generation for 10 different languages and bi-directional streaming.
While gRPC has first class Go support and a stable release of has just been announced, the documentation is still a bit lacking, and it can be unclear how to do some things coming from the JSON/HTTP world. One of them is authorization. How do I deal with my oauth2 tokens? Since gRPC is HTTP2 under the hood, it’s not so different. Let’s take a quick look.
note: this post assumes that you have a basic understanding of how grpc-go works.
How do I issue oauth tokens via gRPC?
You don’t. The oauth standards are defined over simple HTTPS, keep using the same oauth server you had before like dex, hydra or a custom one on top of osin. If for some reason you really need a gRPC interface to generate tokens, translating the different grant methods to gRPC calls should be straight forward.
How do I validate access tokens in my services?
Well, only you know that since it depends on the kind of tokens that your system issues. What you most likely want to know is how to access them within your server implementation.
In HTTP-based services you would often have a middleware which reads the token from the authorization header and validates it before the request is processed. In gRPC, instead of headers you have access to context metadata (actually http headers and trailers under the hood), and there is a similar concept to middleware called interceptor that you can use to wrap your handlers. Beware the metadata keys are always lowercased.
// interceptor function
// https://godoc.org/google.golang.org/grpc#UnaryServerInterceptor
func AuthUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// retrieve metadata from context
md, ok := metadata.FromContext(ctx)
// validate 'authorization' metadata
// like headers, the value is an slice []string
uid, err := MyValidationFunc(md["authorization"])
if err != nil {
return nil, grpc.Errorf(codes.Unauthenticated, "authentication required")
}
// add user ID to the context
newCtx := context.WithValue(ctx, "user_id", uid)
// handle scopes?
// ...
return handler(newCtx, req)
}
// later in your main function
func main() {
...
var opts []grpc.ServerOption
// add the interceptor as a server option
opts = append(opts, grpc.UnaryInterceptor(AuthUnaryInterceptor))
grpcServer := grpc.NewServer(opts...)
...
}
Interceptors are different for unary and streaming calls, and for some reason you can only define one for each kind. But fear not, there are already in interceptor chain implementation ready to use if you need more than one.
How do I send tokens with my requests?
While some would validate the token once and call it a day, if an action is made in behalf of a user I’d highly recommended that the token of the user issuing the request is carried around all the triggered service calls.
If all we want to do is forward the authorization metadata to the next service, we get that out of the box when passing our server context in the client request. Creating new metadata and adding the token is also very simple.
// create new context with metadata
md := metadata.Pairs("authorization", "Bearer XXXX")
ctx := metadata.NewContext(context.Background(), md)
something, err := client.SomeRPCCall(ctx, req)
If you are using the JSON to gRPC gateway to interact with the outside world, you are in luck, as it will automatically convert the authorization header to metadata for you.
In case you want your services to authenticate with each other, meaning the token depends on the caller service, there is an easy to implement PerRPCCredentials interface where you can define a GetRequestMetadata
function which returns default metadata that gets appended to every call. There’s already a few ready-to-use implementations already available for different use cases.
var opts []grpc.DialOption
rpcCred, err := oauth.NewJWTAccessFromFile("/path/to/file")
...
opts = append(opts, grpc.WithPerRPCCredentials(rpcCred))
conn, err := grpc.Dial(*serverAddr, opts...)
...
defer conn.Close()
client := pb.NewServiceClient(conn)
You might also notice the TransportCredentials interface. This is exclusively related to transport security (SSL, TLS), which unless you are running in a completely trusted environment you should enable to keep your tokens from falling into the wrong hands…
var opts []grpc.ServerOption
creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
...
opts = []grpc.ServerOption{grpc.Creds(creds)}
grpcServer := grpc.NewServer(opts...)