38 Authorization with gRPC Interceptors
gRPC has become one of the backbones of modern communication between microservices. Its appeal? Speed, efficiency, and a mature ecosystem. However, once a service starts handling requests from a variety of clients with different access rights, one big issue arises: AUTHORIZATION.
If you hardcode your authorization rules directly into each service handler, problems quickly follow: duplication, code that is hard to maintain, and even hidden security bugs. This is exactly why gRPC Interceptors are such an elegant solution—they act as middleware, allowing you to embed an authorization layer consistently across every endpoint in a clean and testable way.
In this article, I will cover:
- The basic concept of gRPC Interceptors
- What is the difference between “Authorization” and “Authentication”?
- A scheme for implementing authorization with Interceptors
- A simple authorization code example in Go
- Simulating authorization scenarios
- A summary of best practices
Authentication vs. Authorization
Although they sound similar, the two are different. Let’s clarify.
| Authentication | Authorization |
|---|---|
| Who are you? | Is this user granted access? |
| Usually via JWT/Token | Role, permission, policy, etc. |
| Often responds 401/Unauth | Often responds 403/Forbidden |
In most modern applications, authentication happens first. Once the authorization header passes, authorization is then performed to verify whether the user is allowed to access a particular method/endpoint.
In gRPC, you can embed these two steps in separate interceptors, or combine them into a single sequential interceptor.
Getting to Know gRPC Interceptors
Put simply, a gRPC Interceptor is a middleware function that runs before the main method is invoked. It can be installed for:
- Unary Interceptor: For request-response endpoints
- Stream Interceptor: For streaming endpoints (client/server/bidirectional)
For authorization, the Unary Interceptor is the one most commonly used.
A simple flow diagram of authorization within an interceptor can be illustrated as follows:
flowchart TD
req[Permintaan masuk ke gRPC Service]
auth[Autentikasi Token/Identitas]
authorize[Otorisasi: Pengecekan role/permission]
handler[Eksekusi Handler utamanya]
reject[Return Error Forbidden]
req --> auth
auth -- valid --> authorize
auth -- invalid --> reject
authorize -- allowed --> handler
authorize -- denied --> reject
Case Study: A gRPC Service with Authorization
Suppose we have two methods on a Book service:
GetBook: Anyone who is logged in may access itDeleteBook: Only users with theadminrole may access it
Protobuf Service
1service BookService {
2 rpc GetBook(GetBookRequest) returns (BookResponse) {}
3 rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse) {}
4}Implementing an Authorization Interceptor in Golang
Many languages support gRPC, but in the cloud (and startup) ecosystem, Go is a popular choice. Below is an example of a Unary Interceptor for simple authorization using JWT.
1. Decode the Token & Extract the Role
1// A helper function to decode the JWT (without an external library)
2func extractRoleFromToken(authHeader string) (string, error) {
3 // Assume the format "Bearer eyJhbGciOiJIUzI1NiIsInR5cC..."
4 split := strings.Split(authHeader, " ")
5 if len(split) != 2 || split[0] != "Bearer" {
6 return "", errors.New("invalid authorization header")
7 }
8 // Simulate: the role is encoded in the payload section after "."
9 parts := strings.Split(split[1], ".")
10 if len(parts) != 3 {
11 return "", errors.New("malformed jwt")
12 }
13 // In a real use case: parse JSON, base64-decode, etc.
14 // Demo: role 'admin' if token == "admin.token"
15 if split[1] == "admin.token" {
16 return "admin", nil
17 }
18 return "user", nil
19}2. Mapping Permissions to Methods
To keep things clean and flexible, use a map:
1var methodPermission = map[string]string{
2 "/BookService/DeleteBook": "admin",
3 // Other methods, if you want to add a specific role
4}3. The Authorization Interceptor
1import (
2 "google.golang.org/grpc"
3 "google.golang.org/grpc/metadata"
4 "context"
5)
6
7func AuthorizationInterceptor(
8 ctx context.Context,
9 req interface{},
10 info *grpc.UnaryServerInfo,
11 handler grpc.UnaryHandler,
12) (interface{}, error) {
13 // Get the metadata (HTTP header)
14 md, ok := metadata.FromIncomingContext(ctx)
15 if !ok {
16 return nil, status.Error(codes.Unauthenticated, "metadata missing")
17 }
18
19 // Get the Authorization header
20 authHeaders := md["authorization"]
21 if len(authHeaders) == 0 {
22 return nil, status.Error(codes.Unauthenticated, "authorization header missing")
23 }
24
25 userRole, err := extractRoleFromToken(authHeaders[0])
26 if err != nil {
27 return nil, status.Error(codes.Unauthenticated, "invalid token")
28 }
29
30 // Check whether the role is sufficient for this method
31 needRole, ok := methodPermission[info.FullMethod]
32 if ok && userRole != needRole {
33 return nil, status.Error(codes.PermissionDenied, "forbidden")
34 }
35
36 // If it passes, forward to the handler
37 return handler(ctx, req)
38}4. Attach the Interceptor to the Server
1grpcServer := grpc.NewServer(
2 grpc.UnaryInterceptor(AuthorizationInterceptor),
3)
4// register server, etc...Simulating Scenarios: Access & Denied
Scheme Table
| Method | Token | Inferred Role | Outcome |
|---|---|---|---|
| GetBook | Bearer xxx | user | Allowed |
| DeleteBook | Bearer xxx | user | Denied |
| DeleteBook | Bearer admin.token | admin | Allowed |
Pseudo-Test
1// Simulate a request to /BookService/DeleteBook
2
3ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs(
4 "authorization", "Bearer admin.token",
5))
6_, err := AuthorizationInterceptor(ctx, &DeleteBookRequest{}, &grpc.UnaryServerInfo{
7 FullMethod: "/BookService/DeleteBook",
8}, HandlerStub)
9fmt.Println(err) // <nil>, allowed
10
11ctx2 := metadata.NewIncomingContext(context.Background(), metadata.Pairs(
12 "authorization", "Bearer user.token",
13))
14_, err = AuthorizationInterceptor(ctx2, &DeleteBookRequest{}, &grpc.UnaryServerInfo{
15 FullMethod: "/BookService/DeleteBook",
16}, HandlerStub)
17fmt.Println(err) // PermissionDenied, forbiddenFull Flow Diagram
sequenceDiagram
participant Client
participant Interceptor
participant Handler
Client->>Interceptor: gRPC call + header authorization
Interceptor->>Interceptor: Validasi token & mapping method to role
alt Token invalid / tak cukup role
Interceptor-->>Client: Error 401 / 403
else Token valid & role sesuai
Interceptor->>Handler: teruskan request
Handler-->>Client: Return response
end
Best Practices & Notes
- Separate AuthN and AuthZ: Interceptors can be chained, so keep authentication and authorization separate to make maintenance easier.
- Granular: The implementation can be built per-method, per-resource, or even based on dynamic logic.
- Extensible: Integrate with an Identity Provider (OAuth, OIDC, etc.) when you are production-ready.
- Audit & Logging: Add logging for every authorization failure.
- Test Coverage: Create test cases for every combination of role and endpoint.
Conclusion
Authorization with gRPC Interceptors is an industry pattern that is scalable, testable, and elegant—especially in a microservices architecture. Layered security can be built without polluting your handler/core logic.
Start with a static mapping like the one above, then move on to dynamic rules as your organization’s needs grow.
What about you? Is your authorization already modular and maintainable enough across your production gRPC services? If not, perhaps now is the time to refactor—and the Interceptor is your best partner.
References:
Feel free to share your experiences or questions in the comments!