Router-Level Auth Field Stripping for Federated GraphQL
# Router-Level Auth Field Stripping for Federated GraphQL
## Problem
We use schema directives (`@authorize`) to mark fields that require authentication. When an unauthenticated client sends a query containing a mix of public and auth-required fields, the router currently forwards the **entire** query to subgraphs — including the auth-required fields the client cannot access.
```graphql
# Client sends (no JWT):
{
streakBoost { currentDayIndex } # requires @authorize
trendingCollections { name } # public
}
```
Today, both fields reach the subgraph. The subgraph then checks auth, returns `null` + an auth error for `streakBoost`, and returns real data for `trendingCollections`. This works correctly, but the subgraph still receives and processes a field it will always reject — wasting compute.
**What we want:** The router strips auth-required fields from the query before it reaches any subgraph when the request has no valid JWT. The subgraph should only receive:
```graphql
{
trendingCollections { name }
}
```
The router itself would return `null` + an auth error extension for `streakBoost`, matching the response shape the subgraph would have produced.
For the pure case where **all** root fields require auth and there's no JWT, we've already implemented early rejection at the router (the query never reaches the subgraph at all). Field stripping is the natural next step for the mixed case.
## Why This Matters
- **Reduced subgraph load**: Auth-required fields in unauthenticated requests are guaranteed to return null. Forwarding them wastes subgraph CPU, memory, and downstream calls (DB queries, gRPC fanout, etc.).
- **Latency**: The subgraph may do expensive work (resolve nested fields, hit caches) before the auth check rejects the field. Stripping at the router avoids this entirely.
- **Security surface**: Fewer unnecessary requests reaching subgraphs means less attack surface for unauthenticated traffic.
## What We've Built So Far
We have a router plugin (`auth.rs`) that:
1. Extracts and verifies JWTs in `on_graphql_params` / `on_execute`
2. Reads `@authorize` directives from the supergraph schema at startup (`schema_directives.rs`)
3. Walks the operation's `SelectionSet` to determine which root fields require auth
4. Rejects the entire query in `on_execute` when **all** root fields require auth and there's no JWT
The logic to identify which fields require auth already exists. What's missing is the ability to **remove** those fields from the operation before the query planner runs.
## Current Plugin API Constraint
The router plugin lifecycle is:
```
on_graphql_params → on_graphql_parse → on_graphql_validation → on_query_plan → on_execute → on_subgraph_execute → on_subgraph_http_request
```
At every hook after `on_graphql_params`, the operation AST is exposed as an **immutable reference**:
| Hook | Field | Type |
|------|-------|------|
| `on_query_plan` | `filtered_operation_for_plan` | `&OperationDefinition` |
| `on_execute` | `operation_for_plan` | `&OperationDefinition` |
| `on_subgraph_execute` | `execution_request.query` | `&str` |
There is no hook where a plugin can modify the parsed operation AST before query planning.
The only mutable entry points are:
- `on_graphql_params` — can replace the raw query **string** (pre-parse), requiring us to parse, filter, and re-serialize the query ourselves
- `on_subgraph_http_request` — can replace the raw HTTP **body bytes** (post-plan), which is too late and very hacky
Neither is ideal. The natural place to do this is between validation and query planning, operating on the typed AST rather than raw strings.
## Potential Solutions
1. **Mutable operation in `on_query_plan`**: If `filtered_operation_for_plan` were `&mut OperationDefinition`, plugins could call `selection_set.items.retain(...)` to filter fields before planning. Minimal API change.
2. **Operation replacement method**: A `set_operation(OperationDefinition)` method on the `on_query_plan` payload, letting plugins swap in a modified operation. Avoids `&mut` lifetime concerns.
3. **New dedicated hook** (e.g. `on_operation_filter`) between validation and planning, receiving an owned `OperationDefinition` that the plugin returns (possibly modified).
Any of these would let us filter auth-required fields at the AST level — no string manipulation, no re-parsing, and the query planner only generates fetch nodes for fields the client can actually access.
1 条评论