60. Case Study: Authentication Middleware with gRPC
60. Case Study: Authentication Middleware with gRPC
Middleware is one of the crucial components in building scalable and maintainable backend applications. In the context of microservices, middleware is frequently used to handle cross-cutting concerns such as logging, monitoring, rate-limiting, and of course, authentication. In this article, we’ll dissect how to build authentication middleware for a gRPC service, breaking down how it works, the code implementation, and even a simple simulation. This piece is intended for engineers who are already familiar with gRPC and want to integrate authentication into it.
Why Authentication in gRPC?
Unlike a regular HTTP API, gRPC offers high performance through the HTTP/2 protocol and data serialization via Protocol Buffers (protobuf). However, this also means we can’t simply rely on standard HTTP middleware, such as the Express.js framework in NodeJS or the request handler middleware in Django.
To keep communication between microservices secure, the commonly used standards are authentication via JWT (JSON Web Token), API-Key, or even OAuth2. The challenge is that gRPC doesn’t have a middleware concept as expressive as REST APIs, but we can still implement it through interceptors.
Core Concept: Interceptors in gRPC
An interceptor in gRPC is analogous to middleware in an HTTP framework. Interceptors can be used both on the client side (Client Interceptor) and on the server side (Server Interceptor). Authentication is generally performed on the server, especially when validating the requester’s credentials.
1Client ----> Interceptor (Server) ---> HandlerMermaid Diagram:
sequenceDiagram
participant C as Client
participant IS as Interceptor (Server)
participant H as RPC Handler
C->>IS: gRPC Request (metadata+payload)
IS->>IS: Validasi Token/Otentikasi
alt Otentikasi berhasil
IS->>H: Forward Request
H-->>IS: Response
else Gagal otentikasi
IS-->>C: Error: Unauthorized
end
IS-->>C: Response/Error
Case Study: A gRPC Service with JWT Authentication Middleware
Let’s simulate the following case:
UserService that has a single method, GetUserProfile. Only users with a valid JWT token are allowed to access this endpoint.1. Protofile
We’ll start by designing its protobuf file.
1// user.proto
2syntax = "proto3";
3
4service UserService {
5 rpc GetUserProfile(GetUserProfileRequest) returns (UserProfileResponse) {}
6}
7
8message GetUserProfileRequest {
9 string user_id = 1;
10}
11
12message UserProfileResponse {
13 string user_id = 1;
14 string name = 2;
15 string email = 3;
16}2. Implementing the gRPC Server in Go
We choose Go because of its tooling ecosystem and its popularity in microservices environments.
a. Import Dependencies
1import (
2 "context"
3 "errors"
4 "fmt"
5 "log"
6 "net"
7 "strings"
8
9 "github.com/golang-jwt/jwt/v4"
10 "google.golang.org/grpc"
11 "google.golang.org/grpc/metadata"
12)b. JWT Validation Function
1var jwtSecret = []byte("R4has14-4p1-anda")
2
3func validateJWT(tokenStr string) (jwt.MapClaims, error) {
4 token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
5 // Only allow HS256
6 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
7 return nil, fmt.Errorf("Unexpected signing method")
8 }
9 return jwtSecret, nil
10 })
11
12 if err != nil || !token.Valid {
13 return nil, errors.New("invalid token")
14 }
15
16 claims, ok := token.Claims.(jwt.MapClaims)
17 if !ok {
18 return nil, errors.New("invalid claims")
19 }
20 return claims, nil
21}c. Middleware: Unary Interceptor
1func AuthInterceptor(
2 ctx context.Context,
3 req interface{},
4 info *grpc.UnaryServerInfo,
5 handler grpc.UnaryHandler,
6) (interface{}, error) {
7
8 md, ok := metadata.FromIncomingContext(ctx)
9 if !ok {
10 return nil, grpc.Errorf(grpc.Code(grpc.Unauthenticated), "missing metadata")
11 }
12
13 var token string
14 if val, exists := md["authorization"]; exists && len(val) > 0 {
15 parts := strings.SplitN(val[0], " ", 2)
16 if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
17 token = parts[1]
18 }
19 }
20 if token == "" {
21 return nil, grpc.Errorf(grpc.Code(grpc.Unauthenticated), "missing token")
22 }
23
24 claims, err := validateJWT(token)
25 if err != nil {
26 return nil, grpc.Errorf(grpc.Code(grpc.Unauthenticated), "invalid token: %v", err)
27 }
28
29 // Inject claims into the context if needed
30 newCtx := context.WithValue(ctx, "claims", claims)
31
32 // Forward the request to the handler
33 return handler(newCtx, req)
34}d. Registering the Interceptor on the Server
1server := grpc.NewServer(
2 grpc.UnaryInterceptor(AuthInterceptor),
3)e. Implementing UserService
1type userServiceServer struct{}
2
3func (s *userServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserProfileRequest) (*pb.UserProfileResponse, error) {
4 // Retrieve claims from the context (e.g., for multi-role support)
5 userID := req.GetUserId()
6 // Simulate fetching data
7 return &pb.UserProfileResponse{
8 UserId: userID,
9 Name: "Rudi Santoso",
10 Email: "rudi@contoso.com",
11 }, nil
12}Simulating a Request
a. Scenarios
| Scenario | Authorization Header | Result |
|---|---|---|
| Valid token | Bearer abc.valid… | Success |
| Invalid token | Bearer xxx.invalid | Error 401 |
| No token | - | Error 401 |
b. Client Request with Metadata
1md := metadata.Pairs("authorization", "Bearer "+jwtToken)
2ctx := metadata.NewOutgoingContext(context.Background(), md)
3resp, err := client.GetUserProfile(ctx, &pb.GetUserProfileRequest{UserId: "123"})
4if err != nil {
5 log.Fatalf("could not get user profile: %v", err)
6}Explaining the Workflow
Mermaid Flowchart:
flowchart TD
A[Client Request] -->B[Interceptor: Cek Metadata]
B -->|Token tidak ada| F[Error: Unauthenticated]
B -->C{Parse Token}
C -->|Token invalid| F
C -->|Valid| D[Inject claims ke context]
D -->E[Handler: GetUserProfile]
E -->G[Response]
Production Tips
- Use HTTPS (TLS) for the transport layer. JWT only protects authorization, it does not prevent traffic interception!
- Refresh the JWT token if you support auto-login or SSO.
- You can extend the interceptor for multi-role support, claim logging, or rate limiting.
- Monitor gRPC error codes for observability.
Conclusion
Authentication middleware in gRPC can be implemented through the interceptor concept on the server side. By validating the JWT on every request, we can protect all endpoints without the hassle of implementing it in each individual handler. This approach is easy to extend for other needs, such as role-based authorization or metadata logging. As a result, security becomes more centralized and maintainable.
References:
Happy building secure services! For discussion or feedback, don’t hesitate to share in the comments section.