# gRPC Metadata & Authorization Support - Implementation Plan (DRAFT) **Status:** Draft (Option 4 Preferred) **Target:** Q1 2026 **Priority:** High **Complexity:** Medium ## Overview Expose gRPC metadata and `ServerCallContext` to handlers, and integrate the existing `ICommandAuthorizationService` and `IQueryAuthorizationService` with gRPC endpoints. This will enable authentication, authorization, distributed tracing, multi-tenancy, and other context-aware features in gRPC services. ## Problem Statement ### Current Limitations 1. **No Authorization in gRPC:** - `ICommandAuthorizationService` and `IQueryAuthorizationService` exist but are only used by Minimal API - gRPC services skip authorization checks entirely - Security gap for gRPC endpoints 2. **No Context Access:** - Handlers cannot access `ServerCallContext` - Cannot read request metadata (headers, auth tokens, correlation IDs) - Cannot set response metadata - Cannot access user identity/claims - Cannot determine caller information 3. **Authorization Interface Limitation:** ```csharp Task IsAllowedAsync(Type commandType, CancellationToken ct); ``` - No access to user context - No access to request metadata - Cannot implement context-aware authorization ## Goals 1. **Expose Context to Handlers:** Allow handlers to access `ServerCallContext` via accessor service 2. **Integrate Authorization:** Call authorization services before executing handlers in gRPC 3. **Maintain Backward Compatibility:** Existing handlers continue to work without changes 4. **Enable Advanced Scenarios:** Multi-tenancy, distributed tracing, custom authentication 5. **Familiar Pattern:** Use accessor pattern similar to ASP.NET Core's `IHttpContextAccessor` ## Recommended Approach: Context Accessor Pattern (Option 4) **PREFERRED APPROACH** - Use ambient context accessor similar to `IHttpContextAccessor` in ASP.NET Core. ### Why Option 4? - ✅ **Zero breaking changes** - No handler interface modifications - ✅ **Opt-in** - Only inject accessor when context is needed - ✅ **Familiar pattern** - Mirrors `IHttpContextAccessor` developers already know - ✅ **Full protocol access** - Can use raw `ServerCallContext` or `HttpContext` features - ✅ **Flexible** - Handlers that don't need context remain simple ### Implementation Overview ```csharp // New accessor interface public interface ICqrsContextAccessor { ServerCallContext? GrpcContext { get; } HttpContext? HttpContext { get; } } // Handlers opt-in by injecting the accessor public class AddUserCommandHandler : ICommandHandler { private readonly ICqrsContextAccessor _context; public AddUserCommandHandler(ICqrsContextAccessor context) { _context = context; } public async Task HandleAsync(AddUserCommand command, CancellationToken ct) { // Access gRPC metadata when available if (_context.GrpcContext != null) { var token = _context.GrpcContext.RequestHeaders.GetValue("authorization"); var userId = ExtractUserIdFromToken(token); } // Or access HTTP context when available if (_context.HttpContext != null) { var userId = _context.HttpContext.User?.Identity?.Name; } // Handler logic... } } ``` See full implementation details in [Option 4 section](#option-4-context-accessor-pattern-ambient-context) below. --- ## Alternative Design Options (For Reference) ### Option 1: Optional ServerCallContext Parameter **Approach:** Add optional `ServerCallContext` parameter to handler interface methods. **Handler Interface Changes:** ```csharp namespace Svrnty.CQRS.Abstractions; public interface ICommandHandler { Task HandleAsync(TCommand command, CancellationToken cancellationToken = default, ServerCallContext? context = null); } public interface ICommandHandler { Task HandleAsync(TCommand command, CancellationToken cancellationToken = default, ServerCallContext? context = null); } public interface IQueryHandler { Task HandleAsync(TQuery query, CancellationToken cancellationToken = default, ServerCallContext? context = null); } ``` **Pros:** - ✅ Simple and straightforward - ✅ Backward compatible (optional parameter with default null) - ✅ Handlers can explicitly access context when needed - ✅ No dependency on HTTP types in core abstractions **Cons:** - ❌ Couples abstractions to gRPC (requires `Grpc.Core` reference) - ❌ Minimal API handlers won't have access to `HttpContext` through same parameter - ❌ Different context types for HTTP vs gRPC **Authorization Service Changes:** ```csharp namespace Svrnty.CQRS.Abstractions.Security; public interface ICommandAuthorizationService { Task IsAllowedAsync(Type commandType, object? context, CancellationToken ct = default); } ``` Where `context` is: - `ServerCallContext` for gRPC - `HttpContext` for Minimal API - `null` if no context available --- ### Option 2: Unified Context Abstraction **Approach:** Create abstraction over both `ServerCallContext` and `HttpContext`. **New Abstraction:** ```csharp namespace Svrnty.CQRS.Abstractions; public interface IRequestContext { string? GetHeader(string name); void SetHeader(string name, string value); IDictionary Items { get; } CancellationToken CancellationToken { get; } IUserContext? User { get; } } public interface IUserContext { bool IsAuthenticated { get; } string? UserId { get; } IEnumerable Roles { get; } IEnumerable Claims { get; } } public interface ICommandHandler { Task HandleAsync(TCommand command, IRequestContext? context = null, CancellationToken ct = default); } ``` **Implementation Wrappers:** ```csharp internal class GrpcRequestContext : IRequestContext { private readonly ServerCallContext _grpcContext; public GrpcRequestContext(ServerCallContext context) { _grpcContext = context; } public string? GetHeader(string name) => _grpcContext.RequestHeaders.GetValue(name); public CancellationToken CancellationToken => _grpcContext.CancellationToken; // ... map other properties } internal class HttpRequestContext : IRequestContext { private readonly HttpContext _httpContext; // ... similar mapping } ``` **Pros:** - ✅ Protocol-agnostic handlers - ✅ Unified authorization interface - ✅ No coupling to gRPC or HTTP in abstractions - ✅ Can support future protocols (WebSockets, etc.) **Cons:** - ❌ Additional abstraction layer (more complexity) - ❌ Cannot access protocol-specific features (e.g., gRPC deadlines, HTTP response codes) - ❌ Mapping overhead between contexts - ❌ Leaky abstraction if protocol-specific needs arise --- ### Option 3: Separate gRPC-Aware Handler Interfaces **Approach:** Keep existing handlers unchanged, add new gRPC-specific interfaces. **New Interfaces:** ```csharp namespace Svrnty.CQRS.Grpc.Abstractions; public interface IGrpcCommandHandler { Task HandleAsync(TCommand command, ServerCallContext context, CancellationToken ct = default); } public interface IGrpcCommandHandler { Task HandleAsync(TCommand command, ServerCallContext context, CancellationToken ct = default); } public interface IGrpcQueryHandler { Task HandleAsync(TQuery query, ServerCallContext context, CancellationToken ct = default); } ``` **Pros:** - ✅ No changes to core abstractions - ✅ Explicit opt-in for context access - ✅ Can access full `ServerCallContext` API - ✅ Clear separation between gRPC and HTTP handlers **Cons:** - ❌ Handler duplication (need to implement both interfaces if supporting HTTP + gRPC) - ❌ Discovery system needs to handle multiple handler types - ❌ More complex registration (which handler for which protocol?) --- ### Option 4: Context Accessor Pattern (Ambient Context) **Approach:** Use service locator/accessor pattern for context access. **Implementation:** ```csharp namespace Svrnty.CQRS.Abstractions; public interface ICommandContextAccessor { ServerCallContext? GrpcContext { get; } HttpContext? HttpContext { get; } } public class MyCommandHandler : ICommandHandler { private readonly ICommandContextAccessor _contextAccessor; public MyCommandHandler(ICommandContextAccessor contextAccessor) { _contextAccessor = contextAccessor; } public async Task HandleAsync(MyCommand command, CancellationToken ct) { var grpcContext = _contextAccessor.GrpcContext; if (grpcContext != null) { // Access gRPC-specific features } } } ``` **Pros:** - ✅ No changes to handler interfaces - ✅ Fully backward compatible - ✅ Handlers can access context only when needed **Cons:** - ❌ Ambient context is an anti-pattern (hidden dependency) - ❌ Harder to test (need to mock accessor) - ❌ AsyncLocal overhead for context flow - ❌ Not explicit in handler signature --- ## Authorization Integration Regardless of which option is chosen, authorization needs to be integrated into the gRPC source generator. ### Source Generator Changes **File:** `Svrnty.CQRS.Grpc.Generators/ServiceImplementationGenerator.cs` ```csharp public override async Task AddUser( AddUserRequest request, ServerCallContext context) { var command = new AddUserCommand { Name = request.Name, Email = request.Email }; // AUTHORIZATION CHECK var authService = _serviceProvider.GetService(); if (authService != null) { var authResult = await authService.IsAllowedAsync(typeof(AddUserCommand), context, context.CancellationToken); if (authResult == AuthorizationResult.Unauthorized) { throw new RpcException(new Status(StatusCode.Unauthenticated, "Authentication required")); } if (authResult == AuthorizationResult.Forbidden) { throw new RpcException(new Status(StatusCode.PermissionDenied, "Insufficient permissions")); } } // Validation (existing) var validator = _serviceProvider.GetService>(); if (validator != null) { var validationResult = await validator.ValidateAsync(command, context.CancellationToken); if (!validationResult.IsValid) { // ... existing validation error handling } } // Execute handler var handler = _serviceProvider.GetRequiredService>(); var result = await handler.HandleAsync(command, context.CancellationToken, context); // Option 1 return new AddUserResponse { Result = result }; } ``` ### Authorization Service Update **Current Interface:** ```csharp public interface ICommandAuthorizationService { Task IsAllowedAsync(Type commandType, CancellationToken ct = default); } ``` **Proposed Interface:** ```csharp public interface ICommandAuthorizationService { Task IsAllowedAsync( Type commandType, object? requestContext = null, CancellationToken ct = default); } ``` Where `requestContext` can be: - `ServerCallContext` for gRPC - `HttpContext` for HTTP - Custom context for other scenarios - `null` if no context **Breaking Change Mitigation:** Add new method with default parameter to maintain backward compatibility: ```csharp public interface ICommandAuthorizationService { // Old method (deprecated but still supported) Task IsAllowedAsync(Type commandType, CancellationToken ct = default); // New method with context Task IsAllowedAsync(Type commandType, object? requestContext, CancellationToken ct = default); } ``` Default implementation: ```csharp public abstract class CommandAuthorizationServiceBase : ICommandAuthorizationService { public virtual Task IsAllowedAsync(Type commandType, CancellationToken ct = default) { return IsAllowedAsync(commandType, null, ct); } public abstract Task IsAllowedAsync(Type commandType, object? requestContext, CancellationToken ct); } ``` --- ## Implementation Phases ### Phase 1: Context Access (Choose Design Option) - [ ] Decide on design option (1, 2, 3, or 4) - [ ] Update handler interfaces (if needed) - [ ] Update source generator to pass context to handlers - [ ] Create context wrappers/abstractions (if needed) - [ ] Update Minimal API to work with new approach ### Phase 2: Authorization Service Update - [ ] Update `ICommandAuthorizationService` and `IQueryAuthorizationService` interfaces - [ ] Provide backward-compatible base class - [ ] Update Minimal API to pass context to authorization services ### Phase 3: gRPC Authorization Integration - [ ] Update source generator to call authorization services - [ ] Map authorization results to gRPC status codes - [ ] Handle `Unauthenticated` (401) → `StatusCode.Unauthenticated` - [ ] Handle `Forbidden` (403) → `StatusCode.PermissionDenied` ### Phase 4: Sample Implementation - [ ] Create sample authorization service implementation - [ ] Demonstrate JWT token validation in gRPC - [ ] Demonstrate role-based authorization - [ ] Update sample project with authorization examples ### Phase 5: Documentation - [ ] Update CLAUDE.md with authorization details - [ ] Document how to implement custom authorization services - [ ] Document how to access context in handlers - [ ] Create migration guide --- ## Open Questions ### 1. Which design option should we use? **Question:** Option 1 (optional parameter), Option 2 (unified abstraction), Option 3 (separate interfaces), or Option 4 (accessor pattern)? **Decision:** ✅ **Option 4 (Context Accessor Pattern)** - Chosen for backward compatibility and familiar ASP.NET Core pattern. ### 2. Should we support accessing HttpContext in handlers? **Question:** If a handler wants to access HTTP-specific features, should we support that through the same mechanism? **Decision:** ✅ **Yes** - `ICqrsContextAccessor` will expose both `GrpcContext` and `HttpContext` properties. Handlers can check which is non-null to determine the protocol. ### 3. Breaking changes to authorization interface? **Question:** Is it acceptable to add a parameter to the authorization interface? **Options:** - Add new method, keep old one (backward compatible but verbose) - Add optional parameter with default `null` (semi-breaking for implementations) - Create v2 interfaces (`ICommandAuthorizationServiceV2`) ### 4. How should we handle multi-protocol handlers? **Question:** If a handler is used by both HTTP and gRPC, how does it differentiate? **Decision:** ✅ **Check which context is non-null** - Simple null check: `if (_context.GrpcContext != null)` vs `if (_context.HttpContext != null)` ### 5. Should metadata access be strongly typed? **Question:** Should we provide typed accessors for common metadata (JWT, correlation ID, etc.)? **Options:** - Raw string access only: `context.GetHeader("authorization")` - Typed extensions: `context.GetAuthorizationToken()`, `context.GetCorrelationId()` - Dedicated metadata services --- ## Success Criteria ### Functional Requirements - ✅ Handlers can access `ServerCallContext` when needed - ✅ Authorization services are called before gRPC handler execution - ✅ Authorization failures return appropriate gRPC status codes - ✅ Backward compatibility maintained for existing handlers - ✅ Works with both HTTP and gRPC (if unified approach) ### Security Requirements - ✅ Unauthorized requests return `StatusCode.Unauthenticated` - ✅ Forbidden requests return `StatusCode.PermissionDenied` - ✅ Authorization decisions can access user context - ✅ JWT tokens can be validated from gRPC metadata ### Performance Requirements - ✅ Minimal overhead for context passing - ✅ No performance regression for handlers that don't use context --- ## Migration & Backward Compatibility ### Breaking Changes **With Option 4:** ✅ **NO BREAKING CHANGES** - Handler interfaces remain unchanged - Existing handlers work without modification - Authorization interface adds optional parameter (backward compatible) ### Migration Path **For Option 4 (Chosen):** 1. Update to new version 2. All existing handlers continue to work unchanged 3. Handlers that need context inject `ICqrsContextAccessor` via constructor 4. Authorization service implementations can optionally use the new context parameter 5. No code changes required for basic upgrade --- ## Dependencies ### NuGet Packages - `Grpc.AspNetCore` >= 2.68.0 (already referenced) - `Microsoft.AspNetCore.Http.Abstractions` (for `HttpContext`, if using unified abstraction) ### Internal Dependencies - `Svrnty.CQRS.Abstractions` (handler interfaces, authorization interfaces) - `Svrnty.CQRS.Grpc` (context wrappers, if needed) - `Svrnty.CQRS.Grpc.Generators` (source generator updates) - `Svrnty.CQRS.MinimalApi` (authorization integration updates) --- ## Final Recommended Approach Based on preference for **Option 4 (Context Accessor Pattern)**: ### Implementation Summary 1. **Create `ICqrsContextAccessor` interface:** ```csharp public interface ICqrsContextAccessor { ServerCallContext? GrpcContext { get; } HttpContext? HttpContext { get; } } ``` 2. **Implement accessor with AsyncLocal:** - Use `AsyncLocal` to flow context through async calls - Similar to ASP.NET Core's `IHttpContextAccessor` implementation - Set context in gRPC interceptor and HTTP middleware 3. **Update authorization interface** with context parameter: ```csharp Task IsAllowedAsync( Type commandType, object? requestContext = null, CancellationToken ct = default); ``` 4. **Update source generator** to: - Set context in accessor before calling handler - Call authorization service with `ServerCallContext` - Map authorization results to gRPC status codes 5. **Provide sample implementations** for: - JWT token validation from gRPC metadata - Role-based authorization - Multi-tenancy with tenant ID from headers - Correlation ID extraction and propagation ### Advantages - ✅ **Zero breaking changes** - No handler signature modifications required - ✅ **Familiar to ASP.NET Core developers** - Same pattern as `IHttpContextAccessor` - ✅ **Opt-in complexity** - Simple handlers remain simple - ✅ **Full protocol support** - Access raw gRPC and HTTP contexts - ✅ **Testable** - Can mock `ICqrsContextAccessor` in tests ### Next Steps 1. Finalize accessor interface design 2. Implement AsyncLocal-based accessor 3. Update source generator for context setting 4. Integrate authorization checks 5. Create comprehensive samples --- **Document Version:** 0.2 (DRAFT - Option 4 Selected) **Last Updated:** 2025-01-08 **Author:** Svrnty Team **Status:** Ready for Implementation Planning