gRPC Authorization and Roles

Authentication answers the question "Who are you?" Authorization answers the question "What may you do?" A service can know exactly who calls it and still need rules that limit each caller. Roles give those rules a simple, manageable shape.

The Hotel Key Card Analogy

A hotel verifies your identity at check in. The front desk then hands you a key card. The card opens your room and the gym, but it never opens the manager's office. Staff cards open more doors. Authorization works like those card permissions.

Guest card:   Room 204 ✔   Gym ✔   Manager office ✖
Staff card:   Room 204 ✔   Gym ✔   Storage room ✔   Manager office ✖
Manager card: Every door ✔

Two Status Codes

CodeMeaningExample
UNAUTHENTICATEDThe caller proved no identityToken missing or expired
PERMISSION_DENIEDThe caller has an identity but lacks rightsA customer calls an admin method

Return the correct code. Clients react differently to each. UNAUTHENTICATED prompts a new login. PERMISSION_DENIED tells the user to request access.

Role Based Access Control

Role based access control, called RBAC, assigns permissions to roles and roles to users. Managing three roles costs far less effort than managing thousands of individual users.

User Asha ──▶ Role: support ──▶ Permissions: GetOrder, RefundOrder
User Ravi ──▶ Role: admin   ──▶ Permissions: GetOrder, RefundOrder, DeleteOrder
User Meena ─▶ Role: customer ─▶ Permissions: GetOrder

Where the Role Comes From

A trusted earlier step extracts the role and places it in the context. An authentication interceptor validates the token, reads the role claim, and stores the result. The authorization step then reads from the context and never trusts raw request fields.

Authorization Interceptor

One interceptor can guard every method through a table that maps method names to allowed roles.

var methodRoles = map[string][]string{
  "/shop.OrderService/GetOrder":    {"customer", "support", "admin"},
  "/shop.OrderService/RefundOrder": {"support", "admin"},
  "/shop.OrderService/DeleteOrder": {"admin"},
}

func authzInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

  allowed, found := methodRoles[info.FullMethod]
  if !found {
    return nil, status.Error(codes.PermissionDenied, "method not allowed")
  }

  role, ok := roleFromContext(ctx)
  if !ok {
    return nil, status.Error(codes.Unauthenticated, "identity required")
  }

  if !contains(allowed, role) {
    return nil, status.Error(codes.PermissionDenied, "role not permitted")
  }
  return handler(ctx, req)
}

The default behavior matters: a method missing from the table gets denied. Deny by default protects new methods until someone grants access deliberately.

Resource Level Checks

Roles alone fall short in many cases. A customer may read orders, yet only their own orders. Method level rules pass the call through. The handler then compares the order owner with the caller identity.

func (s *server) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
  order, err := s.repo.Find(ctx, req.OrderId)
  if err != nil {
    return nil, status.Error(codes.NotFound, "order not found")
  }
  caller := userIDFromContext(ctx)
  if order.OwnerID != caller && !isStaff(ctx) {
    return nil, status.Error(codes.NotFound, "order not found")
  }
  return toProto(order), nil
}

The code returns NOT_FOUND for another person's order. That choice avoids confirming that the order exists.

Two Layers Working Together

Request ─▶ [Interceptor: role allowed for this method?]
                     │ yes
                     ▼
           [Handler: caller owns this resource?]
                     │ yes
                     ▼
                  Reply

Design Tips

  • Keep the number of roles small and meaningful
  • Name roles after job duties, not after people
  • Store the permission table in one place
  • Test denied cases as carefully as allowed cases
  • Log every denial with the caller and method name

Summary

Authorization decides what an identified caller may do. Use RBAC for method level rules, apply deny by default, and add resource checks inside handlers. Return UNAUTHENTICATED for missing identity and PERMISSION_DENIED for missing rights.