58. gRPC with JWT for Authentication
58. gRPC with JWT for Authentication
gRPC and JWT are two popular technologies for building modern microservices that are both secure and scalable. In this 58th article, I’ll share best practices for implementing gRPC with JWT for authentication, complete with code examples, diagrams, and a simple simulation.
Why gRPC & JWT?
When building a microservice architecture that demands high performance and cross-language interoperability, gRPC is often the go-to choice. gRPC uses Protocol Buffers for data serialization, making inter-service communication lightweight and fast. Even so, security remains a top concern, especially when it comes to authentication & authorization.
This is where JWT (JSON Web Token) comes in as a popular and effective authentication solution. JWT is easy to use, lightweight, and stateless, which makes it a perfect fit for distributed systems and microservices.
Authentication Flow Diagram for gRPC with JWT
Let’s start with a quick overview of the authentication process for a gRPC service using JWT:
sequenceDiagram
participant Client
participant AuthService
participant GRPCServer
Client->>AuthService: Request Token (user & password)
AuthService-->>Client: Return JWT Token
Client->>GRPCServer: gRPC Request (attach JWT)
GRPCServer->>GRPCServer: Validate JWT Token
GRPCServer-->>Client: Response (if valid)
JWT Flow in gRPC
- User Login
The client sends a username and password to the AuthService. - Token Generation
The AuthService creates and returns a JWT if the login succeeds. - Request to the gRPC Service
The client sends a gRPC request to the service it wants to access, attaching the JWT as metadata/header. - JWT Validation
The gRPC service validates the JWT before executing the business logic. - Authorized Response
If the token is valid, the service processes the request and returns a response.
Code: Implementing JWT Auth in gRPC (Golang)
I’ll use Go here, since its ecosystem is mature for implementing gRPC. The principles are the same in other languages.
1. Building the JWT Authentication Service
Let’s start with a simple AuthService: it generates a JWT when the credentials are correct.
1// auth_service.go
2package main
3
4import (
5 "net/http"
6 "time"
7
8 "github.com/golang-jwt/jwt/v4"
9 "github.com/gin-gonic/gin"
10)
11
12var jwtKey = []byte("secret_mykey")
13
14type Credentials struct {
15 Username string `json:"username"`
16 Password string `json:"password"`
17}
18
19func GenerateJWT(username string) (string, error) {
20 claims := &jwt.RegisteredClaims{
21 Subject: username,
22 ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
23 }
24
25 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
26 return token.SignedString(jwtKey)
27}
28
29func Login(c *gin.Context) {
30 var cred Credentials
31 if err := c.BindJSON(&cred); err != nil {
32 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
33 return
34 }
35 // Hardcoded user as an example; use a DB in production!
36 if cred.Username == "alice" && cred.Password == "password123" {
37 tokenString, _ := GenerateJWT(cred.Username)
38 c.JSON(http.StatusOK, gin.H{"token": tokenString})
39 } else {
40 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
41 }
42}
43
44func main() {
45 r := gin.Default()
46 r.POST("/login", Login)
47 r.Run(":8080")
48}2. Defining the gRPC Service (Proto)
1// service.proto
2syntax = "proto3";
3
4service Greeter {
5 rpc SayHello (HelloRequest) returns (HelloReply) {}
6}
7
8message HelloRequest {
9 string name = 1;
10}
11
12message HelloReply {
13 string message = 1;
14}Generate the Go code with:
1protoc --go_out=. --go-grpc_out=. service.proto3. Implementing the JWT Interceptor in gRPC
The interceptor validates the JWT in the metadata of every request.
1// server.go
2package main
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "log"
9 "net"
10
11 "google.golang.org/grpc"
12 "google.golang.org/grpc/metadata"
13 "github.com/golang-jwt/jwt/v4"
14
15 pb "path/to/your/proto"
16)
17
18var jwtKey = []byte("secret_mykey")
19
20func validateJWT(tokenString string) (string, error) {
21 token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
22 return jwtKey, nil
23 })
24 if err != nil {
25 return "", err
26 }
27 if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
28 return claims.Subject, nil
29 }
30 return "", errors.New("invalid token")
31}
32
33// Unary interceptor
34func authInterceptor(
35 ctx context.Context,
36 req interface{},
37 info *grpc.UnaryServerInfo,
38 handler grpc.UnaryHandler,
39) (interface{}, error) {
40 md, ok := metadata.FromIncomingContext(ctx)
41 if !ok {
42 return nil, fmt.Errorf("missing metadata")
43 }
44
45 tokens := md["authorization"]
46 if len(tokens) == 0 {
47 return nil, fmt.Errorf("authorization token is required")
48 }
49 jwtToken := tokens[0]
50 _, err := validateJWT(jwtToken)
51 if err != nil {
52 return nil, fmt.Errorf("invalid token: %v", err)
53 }
54
55 // Forward to handler
56 return handler(ctx, req)
57}
58
59// gRPC Service Implementation
60type server struct {
61 pb.UnimplementedGreeterServer
62}
63
64func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
65 return &pb.HelloReply{Message: "Hello " + in.Name}, nil
66}
67
68func main() {
69 lis, err := net.Listen("tcp", ":50051")
70 if err != nil {
71 log.Fatalf("failed to listen: %v", err)
72 }
73 s := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))
74 pb.RegisterGreeterServer(s, &server{})
75 log.Println("gRPC server started on :50051")
76 if err := s.Serve(lis); err != nil {
77 log.Fatalf("failed to serve: %v", err)
78 }
79}4. Simulation: Calling the gRPC Service with a JWT
A. Obtaining a Token
1curl -X POST http://localhost:8080/login \
2-H "Content-Type: application/json" \
3-d '{"username":"alice", "password":"password123"}'Response:
1{
2 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
3}B. Calling gRPC with a JWT (Go client example)
1import (
2 "context"
3 "log"
4 "google.golang.org/grpc"
5 "google.golang.org/grpc/metadata"
6 pb "path/to/your/proto"
7)
8
9func main() {
10 conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
11 defer conn.Close()
12 client := pb.NewGreeterClient(conn)
13
14 jwtToken := "<token-from-the-previous-step>"
15 md := metadata.New(map[string]string{"authorization": jwtToken})
16 ctx := metadata.NewOutgoingContext(context.Background(), md)
17
18 resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Alice"})
19 if err != nil {
20 log.Fatal("Error: ", err)
21 }
22 log.Println(resp.Message)
23}Table: Comparing gRPC Auth Options
| Auth Option | Stateless | Easy to Integrate | Web Pages (Cookie) | Microservice | Recommended Use |
|---|---|---|---|---|---|
| gRPC + Basic | No | Easy | Yes | Less Secure | Internal service testing |
| gRPC + API Key | Yes | Easy | No | Fairly Secure | Public APIs, low risk |
| gRPC + JWT | Yes | Very Easy | Not directly | Very Strong | Microservices in production |
| gRPC + OAuth2 | Yes | Complex | Yes | Very Strong | OpenAPI, 3rd party, SSO |
Best Practice Tips
- Use HTTPS: Protects against sniffing as the JWT travels between nodes.
- Token Expiry: Always set an expiration time for your JWTs to avoid leaving tokens vulnerable.
- Key Rotation: Rotate
jwtKeyregularly, and use asymmetric JWT signing at larger scale. - Secure Claims: Never put sensitive data inside a JWT!
- Logging & Error Handling: Log every failed authentication request for auditing purposes.
Conclusion
Implementing gRPC with JWT for authentication is one of the foundations of secure, scalable, and stateless microservices. By leveraging an interceptor, we can keep every gRPC endpoint protected from unauthorized access. And remember: security isn’t just about technique, it’s also about discipline in maintaining your application’s ecosystem!
If you’d like the complete source code or have further questions, feel free to leave a comment! 👋
I hope this was helpful. Don’t forget to share it if you found it useful! 🚀