19 KiB
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
-
No Authorization in gRPC:
ICommandAuthorizationServiceandIQueryAuthorizationServiceexist but are only used by Minimal API- gRPC services skip authorization checks entirely
- Security gap for gRPC endpoints
-
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
- Handlers cannot access
-
Authorization Interface Limitation:
Task<AuthorizationResult> IsAllowedAsync(Type commandType, CancellationToken ct);- No access to user context
- No access to request metadata
- Cannot implement context-aware authorization
Goals
- Expose Context to Handlers: Allow handlers to access
ServerCallContextvia accessor service - Integrate Authorization: Call authorization services before executing handlers in gRPC
- Maintain Backward Compatibility: Existing handlers continue to work without changes
- Enable Advanced Scenarios: Multi-tenancy, distributed tracing, custom authentication
- 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
IHttpContextAccessordevelopers already know - ✅ Full protocol access - Can use raw
ServerCallContextorHttpContextfeatures - ✅ Flexible - Handlers that don't need context remain simple
Implementation Overview
// New accessor interface
public interface ICqrsContextAccessor
{
ServerCallContext? GrpcContext { get; }
HttpContext? HttpContext { get; }
}
// Handlers opt-in by injecting the accessor
public class AddUserCommandHandler : ICommandHandler<AddUserCommand, int>
{
private readonly ICqrsContextAccessor _context;
public AddUserCommandHandler(ICqrsContextAccessor context)
{
_context = context;
}
public async Task<int> 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 below.
Alternative Design Options (For Reference)
Option 1: Optional ServerCallContext Parameter
Approach: Add optional ServerCallContext parameter to handler interface methods.
Handler Interface Changes:
namespace Svrnty.CQRS.Abstractions;
public interface ICommandHandler<TCommand>
{
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default, ServerCallContext? context = null);
}
public interface ICommandHandler<TCommand, TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default, ServerCallContext? context = null);
}
public interface IQueryHandler<TQuery, TResult>
{
Task<TResult> 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.Corereference) - ❌ Minimal API handlers won't have access to
HttpContextthrough same parameter - ❌ Different context types for HTTP vs gRPC
Authorization Service Changes:
namespace Svrnty.CQRS.Abstractions.Security;
public interface ICommandAuthorizationService
{
Task<AuthorizationResult> IsAllowedAsync(Type commandType, object? context, CancellationToken ct = default);
}
Where context is:
ServerCallContextfor gRPCHttpContextfor Minimal APInullif no context available
Option 2: Unified Context Abstraction
Approach: Create abstraction over both ServerCallContext and HttpContext.
New Abstraction:
namespace Svrnty.CQRS.Abstractions;
public interface IRequestContext
{
string? GetHeader(string name);
void SetHeader(string name, string value);
IDictionary<string, object> Items { get; }
CancellationToken CancellationToken { get; }
IUserContext? User { get; }
}
public interface IUserContext
{
bool IsAuthenticated { get; }
string? UserId { get; }
IEnumerable<string> Roles { get; }
IEnumerable<Claim> Claims { get; }
}
public interface ICommandHandler<TCommand>
{
Task HandleAsync(TCommand command, IRequestContext? context = null, CancellationToken ct = default);
}
Implementation Wrappers:
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:
namespace Svrnty.CQRS.Grpc.Abstractions;
public interface IGrpcCommandHandler<TCommand>
{
Task HandleAsync(TCommand command, ServerCallContext context, CancellationToken ct = default);
}
public interface IGrpcCommandHandler<TCommand, TResult>
{
Task<TResult> HandleAsync(TCommand command, ServerCallContext context, CancellationToken ct = default);
}
public interface IGrpcQueryHandler<TQuery, TResult>
{
Task<TResult> HandleAsync(TQuery query, ServerCallContext context, CancellationToken ct = default);
}
Pros:
- ✅ No changes to core abstractions
- ✅ Explicit opt-in for context access
- ✅ Can access full
ServerCallContextAPI - ✅ 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:
namespace Svrnty.CQRS.Abstractions;
public interface ICommandContextAccessor
{
ServerCallContext? GrpcContext { get; }
HttpContext? HttpContext { get; }
}
public class MyCommandHandler : ICommandHandler<MyCommand>
{
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
public override async Task<AddUserResponse> AddUser(
AddUserRequest request,
ServerCallContext context)
{
var command = new AddUserCommand
{
Name = request.Name,
Email = request.Email
};
// AUTHORIZATION CHECK
var authService = _serviceProvider.GetService<ICommandAuthorizationService>();
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<IValidator<AddUserCommand>>();
if (validator != null)
{
var validationResult = await validator.ValidateAsync(command, context.CancellationToken);
if (!validationResult.IsValid)
{
// ... existing validation error handling
}
}
// Execute handler
var handler = _serviceProvider.GetRequiredService<ICommandHandler<AddUserCommand, int>>();
var result = await handler.HandleAsync(command, context.CancellationToken, context); // Option 1
return new AddUserResponse { Result = result };
}
Authorization Service Update
Current Interface:
public interface ICommandAuthorizationService
{
Task<AuthorizationResult> IsAllowedAsync(Type commandType, CancellationToken ct = default);
}
Proposed Interface:
public interface ICommandAuthorizationService
{
Task<AuthorizationResult> IsAllowedAsync(
Type commandType,
object? requestContext = null,
CancellationToken ct = default);
}
Where requestContext can be:
ServerCallContextfor gRPCHttpContextfor HTTP- Custom context for other scenarios
nullif no context
Breaking Change Mitigation:
Add new method with default parameter to maintain backward compatibility:
public interface ICommandAuthorizationService
{
// Old method (deprecated but still supported)
Task<AuthorizationResult> IsAllowedAsync(Type commandType, CancellationToken ct = default);
// New method with context
Task<AuthorizationResult> IsAllowedAsync(Type commandType, object? requestContext, CancellationToken ct = default);
}
Default implementation:
public abstract class CommandAuthorizationServiceBase : ICommandAuthorizationService
{
public virtual Task<AuthorizationResult> IsAllowedAsync(Type commandType, CancellationToken ct = default)
{
return IsAllowedAsync(commandType, null, ct);
}
public abstract Task<AuthorizationResult> 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
ICommandAuthorizationServiceandIQueryAuthorizationServiceinterfaces - 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
ServerCallContextwhen 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):
- Update to new version
- All existing handlers continue to work unchanged
- Handlers that need context inject
ICqrsContextAccessorvia constructor - Authorization service implementations can optionally use the new context parameter
- No code changes required for basic upgrade
Dependencies
NuGet Packages
Grpc.AspNetCore>= 2.68.0 (already referenced)Microsoft.AspNetCore.Http.Abstractions(forHttpContext, 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
-
Create
ICqrsContextAccessorinterface:public interface ICqrsContextAccessor { ServerCallContext? GrpcContext { get; } HttpContext? HttpContext { get; } } -
Implement accessor with AsyncLocal:
- Use
AsyncLocal<T>to flow context through async calls - Similar to ASP.NET Core's
IHttpContextAccessorimplementation - Set context in gRPC interceptor and HTTP middleware
- Use
-
Update authorization interface with context parameter:
Task<AuthorizationResult> IsAllowedAsync( Type commandType, object? requestContext = null, CancellationToken ct = default); -
Update source generator to:
- Set context in accessor before calling handler
- Call authorization service with
ServerCallContext - Map authorization results to gRPC status codes
-
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
ICqrsContextAccessorin tests
Next Steps
- Finalize accessor interface design
- Implement AsyncLocal-based accessor
- Update source generator for context setting
- Integrate authorization checks
- Create comprehensive samples
Document Version: 0.2 (DRAFT - Option 4 Selected) Last Updated: 2025-01-08 Author: Svrnty Team Status: Ready for Implementation Planning