Assume that you have a gRPC server and want to identify which client is trying to communicate with it. Fortunately, the gRPC server has ability to provide such feature. A client adds some information on the transport layer and the gRPC server intercepts the request to run identification check. Since all the clients use same SSL certificates for authentication, SSL alone doesn't solve the problem here. In this example we are going to use a static bearer token that represents our client and the server will check it before handling the request.


SSL certificates


First of all you need to create server's SSL certificates.


$ openssl genrsa -out private.key 4096
Generating RSA private key, 4096 bit long modulus
.............++
.............++
(0x10001)

$ openssl req -new -x509 -sha256 -days 1825 -key private.key -out public.crt
Country Name (2 letter code) []:UK
State or Province Name (full name) []:London
Locality Name (eg, city) []:City of London
Organization Name (eg, company) []:You Ltd
Organizational Unit Name (eg, section) []:Engineering
Common Name (eg, fully qualified host name) []:localhost
Email Address []:you@you.com

Structure


When you build a gRPC application, you first create a *.proto file and compile it then start developing your application.


├── Makefile
├── Readme.md
├── client
│   ├── cert
│   │   └── public.crt
│   └── main.go
├── go.mod
├── go.sum
├── pkg
│   └── proto
│   └── credit
│   ├── credit.pb.go
│   └── credit.proto
└── server
├── cert
│   ├── private.key
│   └── public.crt
└── main.go

Files


Makefile


.PHONY: compile
compile: ## Compile the proto file.
protoc -I pkg/proto/credit/ pkg/proto/credit/credit.proto --go_out=plugins=grpc:pkg/proto/credit/

.PHONY: server
server: ## Build and run server.
go build -race -ldflags "-s -w" -o bin/server server/main.go
bin/server

.PHONY: client
client: ## Build and run client.
go build -race -ldflags "-s -w" -o bin/client client/main.go
bin/client

credit.proto


syntax = "proto3";

package credit;

message CreditRequest {
float amount = 1;
}

message CreditResponse {
string confirmation = 1;
}

service CreditService {
rpc Credit(CreditRequest) returns (CreditResponse) {}
}

credit.pb.go


I am not adding the content here because it is generated with the command below.


make compile

client/main.go


package main

import (
"context"
"log"
"time"

"github.com/YOU/bank/pkg/proto/credit"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
"golang.org/x/oauth2"
)

func main() {
log.Println("Client running ...")

rpcCreds := oauth.NewOauthAccess(&oauth2.Token{AccessToken: "client-x-id"})
trnCreds, err := credentials.NewClientTLSFromFile("./client/cert/public.crt", "localhost")
if err != nil {
log.Fatalln(err)
}

opts := []grpc.DialOption{
grpc.WithTransportCredentials(trnCreds),
grpc.WithPerRPCCredentials(rpcCreds),
}
opts = append(opts, grpc.WithBlock())

conn, err := grpc.Dial(":50051", opts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

client := credit.NewCreditServiceClient(conn)

request := &credit.CreditRequest{Amount: 1990.01}

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

response, err := client.Credit(ctx, request)
if err != nil {
log.Fatalln(err)
}

log.Println("Response:", response.GetConfirmation())
}

server/main.go


package main

import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"strings"

"github.com/YOU/bank/pkg/proto/credit"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

type server struct {
credit.UnimplementedCreditServiceServer
}

func main() {
log.Println("Server running ...")

cert, err := tls.LoadX509KeyPair("./server/cert/public.crt", "./server/cert/private.key")
if err != nil {
log.Fatalf("failed to load key pair: %s", err)
}
opts := []grpc.ServerOption{
// Intercept request to check the token.
grpc.UnaryInterceptor(validateToken),
// Enable TLS for all incoming connections.
grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
}

lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

srv := grpc.NewServer(opts...)
credit.RegisterCreditServiceServer(srv, &server{})

log.Fatalln(srv.Serve(lis))
}

func (s *server) Credit(ctx context.Context, request *credit.CreditRequest) (*credit.CreditResponse, error) {
log.Println(fmt.Sprintf("Request: %g", request.GetAmount()))

return &credit.CreditResponse{Confirmation: fmt.Sprintf("Credited %g", request.GetAmount())}, nil
}

func validateToken(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}

if !valid(md["authorization"]) {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}

return handler(ctx, req)
}

func valid(authorization []string) bool {
if len(authorization) < 1 {
return false
}

token := strings.TrimPrefix(authorization[0], "Bearer ")

// If you have more than one client then you will have to update this line.
return token == "client-x-id"
}

Test


$ make server
go build -race -ldflags "-s -w" -o bin/server server/main.go
bin/server
2020/04/04 18:07:37 Server running ...

$ make client
go build -race -ldflags "-s -w" -o bin/client client/main.go
bin/client
2020/04/04 18:07:42 Client running ...
2020/04/04 18:07:42 Response: Credited 1990.01

When you run client code above, the server will also output message below.


2020/04/04 18:07:42 Request: 1990.01

If you change token to something else, the client will output message below


2020/04/04 21:47:21 rpc error: code = Unauthenticated desc = invalid token
make: *** [client] Error 1

References