tutorial

  1. gRPC dengan JWT untuk Otentikasi

58. gRPC dengan JWT untuk Otentikasi

gRPC dan JWT adalah dua teknologi populer dalam membangun microservices modern yang aman dan scalable. Pada artikel ke-58 ini, saya akan membagikan praktik terbaik implementasi gRPC dengan JWT untuk otentikasi, disertai contoh kode, diagram, dan simulasi sederhana.


Mengapa gRPC & JWT?

Ketika membangun arsitektur microservice yang butuh performa tinggi dan interoperabilitas lintas bahasa, gRPC sering jadi pilihan utama. gRPC menggunakan Protocol Buffers untuk serialisasi data, sehingga komunikasi antar-service jadi ringan dan cepat. Namun, keamanan tetap jadi perhatian utama, terutama soal authentication & authorization.

Di sini, JWT (JSON Web Token) hadir sebagai salah satu solusi otentikasi yang populer dan efektif. JWT mudah digunakan, ringan, serta stateless, cocok sekali untuk distributed systems dan microservice.


Diagram Alur Otentikasi gRPC dengan JWT

Mari lihat dulu secuil gambaran proses otentikasi pada gRPC service dengan 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)

Flow JWT pada gRPC

  1. User Login
    Client mengirim username dan password ke AuthService.
  2. Token Generation
    AuthService membuat dan mengirim JWT jika login sukses.
  3. Request ke gRPC Service
    Client mengirim request gRPC ke service yang ingin diakses dengan JWT sebagai metadata/header.
  4. JWT Validation
    gRPC service memvalidasi JWT sebelum mengeksekusi bisnis proses.
  5. Authorized Response
    Jika valid, service akan memproses request dan mengirim response.

Kode: Implementasi JWT Auth pada gRPC (Golang)

Saya gunakan Go, karena ekosistemnya matang untuk implementasi gRPC. Prinsipnya sama di bahasa lain.

1. Membuat Service Authentication JWT

Mari mulai dari AuthService sederhana: menghasilkan JWT jika kredensial benar.

// auth_service.go
package main

import (
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v4"
    "github.com/gin-gonic/gin"
)

var jwtKey = []byte("secret_mykey")

type Credentials struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

func GenerateJWT(username string) (string, error) {
    claims := &jwt.RegisteredClaims{
        Subject:   username,
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

func Login(c *gin.Context) {
    var cred Credentials
    if err := c.BindJSON(&cred); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }
    // Contoh hardcode user, production pakai DB!
    if cred.Username == "alice" && cred.Password == "password123" {
        tokenString, _ := GenerateJWT(cred.Username)
        c.JSON(http.StatusOK, gin.H{"token": tokenString})
    } else {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    }
}

func main() {
    r := gin.Default()
    r.POST("/login", Login)
    r.Run(":8080")
}

2. Mendefinisikan gRPC Service (Proto)

// service.proto
syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Generate kode Go dengan:

protoc --go_out=. --go-grpc_out=. service.proto

3. Implementasi Interceptor JWT pada gRPC

Interceptor akan memvalidasi JWT di metadata setiap request.

// server.go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    "github.com/golang-jwt/jwt/v4"

    pb "path/to/your/proto"
)

var jwtKey = []byte("secret_mykey")

func validateJWT(tokenString string) (string, error) {
    token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtKey, nil
    })
    if err != nil {
        return "", err
    }
    if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
        return claims.Subject, nil
    }
    return "", errors.New("invalid token")
}

// Unary interceptor
func authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, fmt.Errorf("missing metadata")
    }

    tokens := md["authorization"]
    if len(tokens) == 0 {
        return nil, fmt.Errorf("authorization token is required")
    }
    jwtToken := tokens[0]
    _, err := validateJWT(jwtToken)
    if err != nil {
        return nil, fmt.Errorf("invalid token: %v", err)
    }

    // Forward to handler
    return handler(ctx, req)
}

// gRPC Service Implementation
type server struct {
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))
    pb.RegisterGreeterServer(s, &server{})
    log.Println("gRPC server started on :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

4. Simulasi: Call gRPC Service dengan JWT

A. Mendapatkan Token

curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"alice", "password":"password123"}'

Respon:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}

B. Call gRPC dengan JWT (Contoh Go client)

import (
    "context"
    "log"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    pb "path/to/your/proto"
)

func main() {
    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
    defer conn.Close()
    client := pb.NewGreeterClient(conn)

    jwtToken := "<token-dari-step-sebelumnya>"
    md := metadata.New(map[string]string{"authorization": jwtToken})
    ctx := metadata.NewOutgoingContext(context.Background(), md)

    resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Alice"})
    if err != nil {
        log.Fatal("Error: ", err)
    }
    log.Println(resp.Message)
}

Tabel: Perbandingan gRPC Auth

Opsi AuthStatelessMudah IntegrasiHalaman Web (Cookie)MicroserviceSaran Penggunaan
gRPC + BasicNoMudahYaKurang AmanInternal service testing
gRPC + API KeyYesMudahTidakCukup AmanAPI Public low risk
gRPC + JWTYesSangat MudahTidak langsungSangat KuatMicroservice production
gRPC + OAuth2YesKompleksYaSangat KuatOpenAPI, 3rd party, SSO

Tips Best Practice

  • Gunakan HTTPS: Aman dari sniffing ketika JWT berpindah antar-node.
  • Token Expiry: Selalu tentukan masa berlaku JWT, untuk menghindari token rentan.
  • Key Rotation: Rutin rotasi jwtKey, gunakan JWT signing asymmetric pada skala besar.
  • Claim Secure: Jangan menaruh data sensitif di dalam JWT!
  • Logging & Error Handling: Log semua request gagal otentikasi untuk keperluan audit.

Kesimpulan

Mengimplementasi gRPC dengan JWT untuk otentikasi adalah salah satu fondasi microservices yang aman, scalable, dan stateless. Dengan memanfaatkan interceptor, kita dapat menjaga seluruh endpoint gRPC tetap terlindungi dari akses tak berhak. Jangan lupa, keamanan bukan cuma soal teknik, tapi juga disiplin dalam memelihara ekosistem aplikasi!

Jika ingin source code lengkap atau tanya lebih lanjut, feel free tinggalkan komentar! 👋


Semoga bermanfaat. Jangan lupa share jika berguna! 🚀

comments powered by Disqus