Describe how you would implement token-based authentication and authorization in a gRPC service built with Go.
Go & Rust interview question for Advanced practice.
Answer
Implementing authentication and authorization in a Go gRPC service is idiomatically done using interceptors to keep security logic separate from business logic. 1. Authentication (Verifying Identity): This is typically done using bearer tokens (like JWTs or OAuth2 tokens) sent in the request metadata. Client Side: The client attaches the token to the outgoing request context. go md := metadata.Pairs("authorization", "Bearer my-jwt-token") ctx := metadata.NewOutgoingContext(context.Background(), md) client.SomeRPC(ctx, req) // Token is now sent with the RPC Server Side Interceptor: A unary server interceptor extracts the token from the incoming metadata, validates it (e.g., checks the signature and expiry of a JWT), and returns an Unauthenticated error if it's invalid. If valid, it can inject the user's identity (e.g., user ID, roles) into the context for later use by the authorization logic or the handler itself. 2. Authorization (Checking Permissions): This is the process of checking if the authenticated user has permission to perform the requested action. This logic can live in the same interceptor or a separate one that runs after authentication. Server Side Interceptor: After authentication succeeds, the interceptor retrieves the user's identity from the context. It then checks if that user has the required permission for the specific RPC method being called (info.FullMethod). This might involve checking roles against a predefined access control list (ACL) or database. If the check fails, the interceptor returns a PermissionDenied error. go // Conceptual Interceptor Logic func authInterceptor(ctx context.Context, req interface{}, info grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // 1. AUTHENTICATION token, err := extractToken(ctx) // Extract from metadata if err != nil { return nil, status.Error(codes.Unauthenticated, "token not provided") } user, err := validateToken(token) // Validate token if err != nil { return nil, status.Errorf(codes.Unauthenticated, "token is invalid: %v", err) } // 2. AUTHORIZATION if !checkPermissions(user, info.FullMethod) { // Check user's permissions for this method return nil, status.Errorf(codes.PermissionDenied, "user does not have permission for %s", info.FullMethod) } // If both pass, add user to context and call the actual handler newCtx := context.WithValue(ctx, "userKey", user) return handler(newCtx, req) }
Explanation
The 'per-RPC credentials' mechanism in gRPC allows a client to attach authentication metadata (like a bearer token) to each outgoing request, which is the standard way to implement token-based auth.