-
-
Notifications
You must be signed in to change notification settings - Fork 802
[Fusion] Add auth docs #9502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fusion] Add auth docs #9502
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,9 +6,9 @@ title: "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. | ||||||
| The gateway validates tokens, extracts identity, and forwards the relevant information to subgraphs via HTTP headers. Each subgraph then enforces access control on its own fields using its own authorization framework. | ||||||
|
|
||||||
| This page walks through the full auth chain: gateway-level authentication, header propagation, gateway-to-subgraph trust, and subgraph-level authorization. | ||||||
| This page walks through the gateway side of the auth chain: authentication, header propagation, and gateway-to-subgraph trust. | ||||||
|
|
||||||
| ## Gateway-Level Authentication | ||||||
|
|
||||||
|
|
@@ -23,15 +23,10 @@ The gateway is the single entry point for all client requests. It is the right p | |||||
| The most common pattern is JWT (JSON Web Token) authentication. Configure the gateway to validate tokens against your identity provider: | ||||||
|
|
||||||
| ```csharp | ||||||
| // Gateway/Program.cs | ||||||
| using 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 propagation | ||||||
| builder.Services | ||||||
| .AddCors() | ||||||
|
|
@@ -82,16 +77,16 @@ 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). | ||||||
| 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: | ||||||
|
|
||||||
| 1. **Header propagation registration** -- declares which headers the gateway forwards to subgraphs. | ||||||
| 2. **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. | ||||||
| 3. **JWT authentication** -- standard ASP.NET Core JWT Bearer setup. Configure `Authority` and `Audience` for your identity provider (Keycloak, Auth0, Azure AD, etc.). | ||||||
| 4. **Authorization** -- registers the authorization services. | ||||||
| 5. **Fusion gateway** -- registers the gateway with its configuration source. | ||||||
| 6. **Middleware order** -- `UseHeaderPropagation()` must come before `UseAuthentication()` so that the `Authorization` header is captured for propagation before the auth middleware consumes it. Then `UseAuthentication()` must come before `UseAuthorization()`. | ||||||
| 1. **Header propagation registration.** Declares which headers the gateway forwards to subgraphs. | ||||||
| 2. **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. | ||||||
| 3. **JWT authentication.** Standard ASP.NET Core JWT Bearer setup. Configure `Authority` and `Audience` for your identity provider (Keycloak, Auth0, Azure AD, etc.). | ||||||
| 4. **Authorization.** Registers the authorization services. | ||||||
| 5. **Fusion gateway.** Registers the gateway with its configuration source. | ||||||
| 6. **Middleware order.** `UseHeaderPropagation()` must come before `UseAuthentication()` so that the `Authorization` header is captured for propagation before the auth middleware consumes it. Then `UseAuthentication()` must come before `UseAuthorization()`. | ||||||
|
|
||||||
| ### Allowing Anonymous Access | ||||||
|
|
||||||
|
|
@@ -104,7 +99,7 @@ builder.Services.AddAuthorization(options => | |||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| With this configuration, unauthenticated requests pass through the gateway to subgraphs. Individual subgraph fields can then require authentication using `[Authorize]`. | ||||||
| With this configuration, unauthenticated requests pass through the gateway to subgraphs. Individual subgraph fields can then enforce their own authorization rules as needed. | ||||||
|
|
||||||
| ## Header Propagation | ||||||
|
|
||||||
|
|
@@ -198,7 +193,7 @@ In a production deployment, subgraphs should not be publicly accessible. The gat | |||||
| 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. | ||||||
| - 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 | ||||||
|
|
@@ -235,176 +230,11 @@ app.Use(async (context, 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`: | ||||||
|
|
||||||
| ```csharp | ||||||
| // Products/Program.cs | ||||||
| using 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: | ||||||
| ## Subgraph Authorization | ||||||
|
|
||||||
| - **`AddGraphQL("products-api")`** is the named schema pattern used in production setups. The string argument is the schema name that matches the subgraph's `schema-settings.json` name. This is equivalent to `AddGraphQLServer()` used in the Getting Started tutorial -- `AddGraphQLServer()` is shorthand for an unnamed schema, while `AddGraphQL("name")` gives the schema an explicit name. Both work the same way for authorization. | ||||||
| - **`.AddAuthorization()`** on the GraphQL builder (not just `builder.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 `Authorization` header against the same identity provider. | ||||||
| - Subgraphs receive the raw `Authorization` header from the gateway via header propagation, so the JWT middleware validates the same token the gateway already validated. | ||||||
| Authorization is a subgraph concern. Since each subgraph is a standalone GraphQL server, you configure authorization using whatever framework your subgraph is built with. The gateway's role ends at forwarding identity information via headers. How subgraphs enforce access control is up to them. | ||||||
|
|
||||||
| ### Field-Level Authorization | ||||||
|
|
||||||
| Apply `[Authorize]` to restrict access to specific fields or types: | ||||||
|
|
||||||
| ```csharp | ||||||
| [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]`: | ||||||
|
|
||||||
| ```csharp | ||||||
| // Program.cs | ||||||
| builder.Services.AddAuthorization(options => | ||||||
| { | ||||||
| options.AddPolicy("AdminOnly", policy => | ||||||
| policy.RequireClaim("role", "admin")); | ||||||
|
|
||||||
| options.AddPolicy("CanManageProducts", policy => | ||||||
| policy.RequireClaim("permissions", "products:write")); | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| ```csharp | ||||||
| [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: | ||||||
|
|
||||||
| ```csharp | ||||||
| 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: | ||||||
|
|
||||||
| ```csharp | ||||||
| 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: | ||||||
|
|
||||||
| ```csharp | ||||||
| // Gateway/Program.cs | ||||||
| builder.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: | ||||||
|
|
||||||
| ```csharp | ||||||
| // Subgraph middleware or resolver | ||||||
| public 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); | ||||||
| } | ||||||
| ``` | ||||||
| For subgraphs built with HotChocolate, see the [HotChocolate Authorization docs](/docs/hotchocolate/v16/securing-your-api/authorization) for a complete guide on field-level authorization, policies, and claims-based access control. | ||||||
|
|
||||||
| ### What Happens When Auth Fails Mid-Query | ||||||
|
|
||||||
|
|
@@ -431,7 +261,7 @@ When a query touches multiple subgraphs and authorization fails on one field, th | |||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| 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. | ||||||
| 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 | ||||||
|
|
||||||
|
|
@@ -484,15 +314,8 @@ app.MapGraphQL(); // Serve the GraphQL endpoint | |||||
| app.Run(); | ||||||
| ``` | ||||||
|
|
||||||
| ### Known Issues | ||||||
|
|
||||||
| **`ApplyPolicy` type name collision ([GitHub #6333](https://github.com/ChilliCream/graphql-platform/issues/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](https://github.com/ChilliCream/graphql-platform/issues/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](/docs/fusion/v16/deployment-and-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 before `UseAuthentication()` in the pipeline. | ||||||
| - **"I need to set up subgraph authorization."** The [HotChocolate Authorization docs](/docs/hotchocolate/v16/securing-your-api/authorization) cover field-level authorization, policies, and claims-based access control for HotChocolate subgraphs. | ||||||
| - **"I need to deploy this securely."** [Deployment & CI/CD](/docs/fusion/v16/deployment-and-ci-cd) covers production deployment patterns including network isolation and CI pipeline setup. | ||||||
| - **"Something is broken."** Check the middleware order section above and ensure `UseHeaderPropagation()` appears before `UseAuthentication()` in the pipeline. | ||||||
|
||||||
| - **"Something is broken."** Check the middleware order section above and ensure `UseHeaderPropagation()` appears before `UseAuthentication()` in the pipeline. | |
| - **"Something is broken."** Check the middleware order section above and verify the order matches your propagation strategy: for raw header forwarding such as `Authorization`, there is no strict requirement that `UseHeaderPropagation()` run before `UseAuthentication()`, but for propagated headers derived from authenticated user claims, `UseAuthentication()` must run before `UseHeaderPropagation()` so `HttpContext.User` is populated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The middleware-order guidance here is internally inconsistent with the later “Custom Header Injection” example that derives propagated headers from
HttpContext.User(which is populated byUseAuthentication()). Authentication middleware does not consume/remove theAuthorizationheader, soUseHeaderPropagation()does not need to run beforeUseAuthentication()to forward it; if anything, when propagating derived headers from claims it should run after authentication (and beforeMapGraphQL()). Please adjust the wording (and/or the pipeline snippet) to reflect the correct ordering rationale.