See the latest stable version instead.
Authentication and Authorization
Authentication and Authorization
The mental model for auth in Fusion is straightforward: authentication terminates at the gateway, authorization is a subgraph concern.
The gateway validates tokens, extracts identity, and forwards the relevant information to subgraphs via HTTP headers. Each subgraph then uses standard HotChocolate authorization ([Authorize], policies, claims) to enforce access control on its own fields. There is nothing Fusion-specific about subgraph authorization -- your subgraph is a HotChocolate server, and you use the same auth patterns you already know.
This page walks through the full auth chain: gateway-level authentication, header propagation, gateway-to-subgraph trust, and subgraph-level authorization.
Gateway-Level Authentication
The gateway is the single entry point for all client requests. It is the right place to validate authentication tokens because:
- Clients only talk to the gateway. They never reach subgraphs directly.
- Token validation happens once, not N times across N subgraphs.
- Invalid requests are rejected before any subgraph work begins.
JWT Bearer Setup
The most common pattern is JWT (JSON Web Token) authentication. Configure the gateway to validate tokens against your identity provider:
// Gateway/Program.csusing Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
// AddServiceDefaults registers shared configuration (OpenTelemetry, health checks, etc.)// from a shared defaults project. Remove this line if your project does not use shared defaults.builder.AddServiceDefaults("gateway-api", "1.0.0");
// 1. Register header propagationbuilder.Services .AddCors() .AddHeaderPropagation(c => { c.Headers.Add("GraphQL-Preflight"); c.Headers.Add("Authorization"); });
// 2. Configure the named HTTP client for subgraph communicationbuilder.Services .AddHttpClient("fusion") .AddHeaderPropagation();
// 3. Set up JWT authenticationbuilder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://your-identity-provider.com/realms/your-realm"; options.Audience = "graphql-api"; options.TokenValidationParameters = new() { ValidateAudience = true, ValidateIssuer = true, ValidateLifetime = true }; });
// 4. Set up authorizationbuilder.Services.AddAuthorization();
// 5. Configure the Fusion gatewaybuilder .AddGraphQLGateway() .AddFileSystemConfiguration("./gateway.far");
var app = builder.Build();
app.UseCors(c => c.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
// 6. Middleware order mattersapp.UseHeaderPropagation();app.UseAuthentication();app.UseAuthorization();
app.MapGraphQL();app.Run();
This is standard ASP.NET Core authentication -- the only Fusion-specific parts are AddGraphQLGateway() and the header propagation setup (covered next).
The numbered comments highlight the six key pieces:
- Header propagation registration -- declares which headers the gateway forwards to subgraphs.
- Named HTTP client
"fusion"-- this is the client the gateway uses to call subgraphs. Adding.AddHeaderPropagation()to it ensures the declared headers are forwarded on every subgraph request. - JWT authentication -- standard ASP.NET Core JWT Bearer setup. Configure
AuthorityandAudiencefor your identity provider (Keycloak, Auth0, Azure AD, etc.). - Authorization -- registers the authorization services.
- Fusion gateway -- registers the gateway with its configuration source.
- Middleware order --
UseHeaderPropagation()must come beforeUseAuthentication()so that theAuthorizationheader is captured for propagation before the auth middleware consumes it. ThenUseAuthentication()must come beforeUseAuthorization().
Allowing Anonymous Access
By default, ASP.NET Core does not require authentication unless you configure a fallback policy. If you want some queries to be accessible without a token (while still validating tokens when present), do not set a restrictive fallback:
builder.Services.AddAuthorization(options =>{ options.FallbackPolicy = null; // Allow anonymous by default});
With this configuration, unauthenticated requests pass through the gateway to subgraphs. Individual subgraph fields can then require authentication using [Authorize].
Header Propagation
Header propagation is how authentication context travels from the gateway to subgraphs. When a client sends a request with an Authorization: Bearer <token> header, the gateway forwards that header to each subgraph it calls during query execution.
How It Works
The setup involves two parts:
1. Declare which headers to propagate:
builder.Services.AddHeaderPropagation(c =>{ c.Headers.Add("GraphQL-Preflight"); c.Headers.Add("Authorization");});
This tells the header propagation middleware to capture these headers from incoming requests and make them available for outgoing requests.
2. Attach propagation to the named HTTP client:
builder.Services .AddHttpClient("fusion") .AddHeaderPropagation();
The name "fusion" matters. This is the default named HTTP client that the Fusion gateway uses to communicate with subgraphs. The name must match the clientName in your subgraphs' schema-settings.json files (which defaults to "fusion" if not specified):
{ "name": "products-api", "transports": { "http": { "clientName": "fusion", "url": "http://localhost:5001/graphql" } }}
When the gateway makes HTTP requests to subgraphs, the "fusion" client automatically includes the propagated headers.
Custom Header Injection
Sometimes you need to forward derived information rather than raw headers. For example, after validating a JWT token, you might want to inject the user's ID or tenant ID as custom headers:
builder.Services.AddHeaderPropagation(c =>{ c.Headers.Add("Authorization");
// Inject custom headers derived from the authenticated user c.Headers.Add("X-User-Id", context => { var userId = context.HttpContext.User.FindFirst("sub")?.Value; return new StringValues(userId); });
c.Headers.Add("X-Tenant-Id", context => { var tenantId = context.HttpContext.User.FindFirst("tenant_id")?.Value; return new StringValues(tenantId); });});
Subgraphs can then read these headers from HttpContext:
// In a subgraph resolverpublic static Product? GetProductById( int id, IHttpContextAccessor httpContextAccessor){ var tenantId = httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].ToString(); // Use tenantId for data filtering...}
This pattern is useful when subgraphs should not parse JWT tokens themselves but still need identity information.
Gateway-to-Subgraph Trust
In a production deployment, subgraphs should not be publicly accessible. The gateway is the only service clients talk to; subgraphs are internal services.
Why This Matters
If subgraphs are publicly accessible:
- Clients could bypass the gateway and query subgraphs directly, skipping authentication.
- Internal lookups (marked
[Internal]) would be exposed -- they are hidden from the composite schema but still exist as real HTTP endpoints on the subgraph. - Rate limiting, query complexity analysis, and other gateway-level protections are bypassed.
Network Isolation Options
Private network (recommended): Deploy subgraphs in a private network (VPC, internal Kubernetes service, Azure VNet) that the gateway can reach but external clients cannot. This is the simplest and most secure option.
Mutual TLS (mTLS): Configure TLS certificates on both the gateway and subgraphs so they authenticate each other. This adds transport-level security even within a private network.
Shared secret / API key: Add a custom header (like X-Internal-Api-Key) via header propagation and validate it in subgraph middleware. This is simpler than mTLS but less secure:
// Gateway: inject a shared secret headerbuilder.Services.AddHeaderPropagation(c =>{ c.Headers.Add("Authorization"); c.Headers.Add("X-Internal-Api-Key", _ => new StringValues("your-shared-secret"));});
// Subgraph: validate the shared secret in middlewareapp.Use(async (context, next) =>{ var apiKey = context.Request.Headers["X-Internal-Api-Key"].ToString(); if (apiKey != "your-shared-secret") { context.Response.StatusCode = 401; return; } await next();});
In practice, most teams use private networking and do not need additional subgraph-level authentication for internal traffic. Choose the approach that matches your infrastructure and security requirements.
Subgraph-Level Authorization
Once headers arrive at a subgraph, authorization is entirely standard HotChocolate. There is nothing Fusion-specific here -- your subgraph is a HotChocolate server, and you use the same [Authorize] patterns you would use on a standalone GraphQL API.
Subgraph Auth Setup
Each subgraph configures authentication and authorization in its Program.cs:
// Products/Program.csusing Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://your-identity-provider.com/realms/your-realm"; options.Audience = "graphql-api"; options.RequireHttpsMetadata = false; // For local development only options.TokenValidationParameters = new() { ValidateAudience = false, ValidateIssuer = true, ValidateLifetime = true }; });
builder.Services.AddAuthorization();
builder .AddGraphQL("products-api") .AddAuthorization() // Enables HotChocolate's [Authorize] attribute .AddTypes();
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();app.MapGraphQL();
app.RunWithGraphQLCommands(args);
Key details:
AddGraphQL("products-api")is the named schema pattern used in production setups. The string argument is the schema name that matches the subgraph'sschema-settings.jsonname. This is equivalent toAddGraphQLServer()used in the Getting Started tutorial --AddGraphQLServer()is shorthand for an unnamed schema, whileAddGraphQL("name")gives the schema an explicit name. Both work the same way for authorization..AddAuthorization()on the GraphQL builder (not justbuilder.Services) enables HotChocolate's authorization integration, which makes[Authorize]work on GraphQL fields and types.- The JWT configuration can mirror the gateway's configuration, or the subgraph can validate the forwarded
Authorizationheader against the same identity provider. - Subgraphs receive the raw
Authorizationheader from the gateway via header propagation, so the JWT middleware validates the same token the gateway already validated.
Using [Authorize] on Fields
Apply [Authorize] to restrict access to specific fields or types:
[QueryType]public static partial class ProductQueries{ // Anyone can browse products [Lookup, NodeResolver] public static async Task<Product?> GetProductByIdAsync( int id, IProductByIdDataLoader productById, CancellationToken cancellationToken) => await productById.LoadAsync(id, cancellationToken);
// Only authenticated users can see pricing analytics [Authorize] public static PricingAnalytics GetPricingAnalytics(int productId) => AnalyticsService.GetForProduct(productId);}
Policy-Based Authorization
For more granular control, define authorization policies and reference them in [Authorize]:
// Program.csbuilder.Services.AddAuthorization(options =>{ options.AddPolicy("AdminOnly", policy => policy.RequireClaim("role", "admin"));
options.AddPolicy("CanManageProducts", policy => policy.RequireClaim("permissions", "products:write"));});
[MutationType]public static partial class ProductMutations{ [Authorize(Policy = "CanManageProducts")] public static async Task<Product> UpdateProductAsync( int productId, string name, double price, ProductContext context, CancellationToken cancellationToken) { // Only users with "products:write" permission reach here var product = await context.Products.FindAsync(productId); product!.Name = name; product.Price = price; await context.SaveChangesAsync(cancellationToken); return product; }}
How Claims and Headers Arrive
When the gateway propagates the Authorization header, the subgraph's JWT middleware validates the token and populates HttpContext.User with the token's claims. You access claims the same way you would in any ASP.NET Core application:
public static UserProfile GetMyProfile( IHttpContextAccessor httpContextAccessor){ var user = httpContextAccessor.HttpContext?.User; var userId = user?.FindFirst("sub")?.Value; var email = user?.FindFirst("email")?.Value; // ...}
If you injected custom headers at the gateway (like X-User-Id or X-Tenant-Id), read them from the request:
var tenantId = httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].ToString();
Common Patterns
Multi-Tenant Header Propagation
For multi-tenant applications, extract the tenant identifier at the gateway and propagate it as a header so every subgraph can filter data by tenant:
// Gateway/Program.csbuilder.Services.AddHeaderPropagation(c =>{ c.Headers.Add("Authorization"); c.Headers.Add("X-Tenant-Id", context => { // Extract tenant from the JWT token's claims var tenantId = context.HttpContext.User.FindFirst("tenant_id")?.Value ?? "default"; return new StringValues(tenantId); });});
Each subgraph reads the tenant header and applies it as a data filter:
// Subgraph middleware or resolverpublic static async Task<List<Product>> GetProducts( IHttpContextAccessor httpContextAccessor, ProductContext context, CancellationToken cancellationToken){ var tenantId = httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].ToString(); return await context.Products .Where(p => p.TenantId == tenantId) .ToListAsync(cancellationToken);}
What Happens When Auth Fails Mid-Query
When a query touches multiple subgraphs and authorization fails on one field, the gateway returns a partial result. Authorized fields resolve normally, and unauthorized fields return null with an error in the errors array:
{ "data": { "productById": { "name": "Table", "price": 899.99, "costBreakdown": null } }, "errors": [ { "message": "The current user is not authorized to access this resource.", "path": ["productById", "costBreakdown"], "extensions": { "code": "AUTH_NOT_AUTHORIZED" } } ]}
This is standard GraphQL error behavior -- the unauthorized field returns null, and the rest of the query succeeds. Design your schema accordingly: fields that might be unauthorized should be nullable so the query can still return useful data even when some fields are denied.
The Full Auth Middleware Chain
Here is the complete middleware chain for a gateway with JWT authentication and header propagation, annotated with what each step does:
// Gateway/Program.csvar builder = WebApplication.CreateBuilder(args);
// --- Service Registration ---
// Propagate headers to subgraphsbuilder.Services .AddHeaderPropagation(c => { c.Headers.Add("GraphQL-Preflight"); c.Headers.Add("Authorization"); });
// Named HTTP client for subgraph communicationbuilder.Services .AddHttpClient("fusion") .AddHeaderPropagation();
// JWT token validationbuilder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://your-idp.com/realms/your-realm"; options.Audience = "graphql-api"; });
// Authorization policiesbuilder.Services.AddAuthorization();
// Fusion gatewaybuilder .AddGraphQLGateway() .AddFileSystemConfiguration("./gateway.far");
var app = builder.Build();
// --- Middleware Pipeline (order matters!) ---
app.UseHeaderPropagation(); // Capture headers for forwardingapp.UseAuthentication(); // Validate JWT tokensapp.UseAuthorization(); // Enforce authorization policiesapp.MapGraphQL(); // Serve the GraphQL endpointapp.Run();
Known Issues
ApplyPolicy type name collision (GitHub #6333): When both the gateway and subgraphs register authorization, the ApplyPolicy enum type can collide during composition. The workaround is to ensure authorization configuration is consistent across all services. This issue is tracked and being addressed.
Header propagation with InitializeOnStartup (GitHub #5547): In some configurations, header propagation may not work correctly when the gateway initializes eagerly. If you encounter missing headers on subgraph requests, verify that UseHeaderPropagation() appears before UseAuthentication() in the middleware pipeline, and that the "fusion" HTTP client has .AddHeaderPropagation() attached.
Next Steps
- "I need to deploy this securely" -- Deployment & CI/CD covers production deployment patterns including network isolation and CI pipeline setup.
- "I need to handle subgraph failures gracefully" -- Error handling and resilience, including partial results and retry policies, will be covered in future documentation.
- "I want to monitor auth-related issues" -- Monitoring and observability, including distributed tracing across gateway and subgraphs, will be covered in future documentation.
- "Something is broken" -- Check the middleware order section above and ensure
UseHeaderPropagation()appears beforeUseAuthentication()in the pipeline.