diff --git a/packages/web/docs/src/content/router/configuration/authorization.mdx b/packages/web/docs/src/content/router/configuration/authorization.mdx new file mode 100644 index 00000000000..a0b546b95bd --- /dev/null +++ b/packages/web/docs/src/content/router/configuration/authorization.mdx @@ -0,0 +1,174 @@ +--- +title: 'authorization' +--- + +# authorization + +The `authorization` configuration lets you control fine-grained access to your GraphQL schema using +directives. This allows you to restrict which fields authenticated users can access based on their +authentication status or specific scopes. + +For practical examples and a guide to authorization concepts, see +**["Authorization"](../security/authorization)** in the documentation. + +## How Authorization Works + +The router supports two modes for handling unauthorized field access: + +- **`filter` (default mode):** This mode silently removes any fields from the incoming GraphQL + operation that the user is not authorized to access. For each field removed, a corresponding + authorization error is added to the `errors` list in the GraphQL response, while the rest of + the accessible data is returned as requested. +- **`reject` mode:** In this mode, the router will reject any GraphQL operation that attempts to + access one or more unauthorized fields. The entire request is denied, and a descriptive error is + returned. + +## Directives + +Access control is defined within the supergraph schema using the following directives: + +### `@authenticated` + +Restricts access to a field to only authenticated users. Any request without valid authentication +(such as requests without a JWT token) will be prevented from accessing the field. + +**Usage:** + +```graphql +type Query { + me: User @authenticated + publicData: String +} +``` + +### `@requiresScopes(scopes: [[String]])` + +Provides more granular control by requiring the user to possess specific scopes. The directive +supports: + +- **`AND` logic** for scopes within a nested list (e.g., `[["read:users", "write:users"]]`) - the + user must have all scopes in the list +- **`OR` logic** for scopes across nested lists (e.g., `[["read:users"], ["admin:users"]]`) - the + user must have scopes from at least one of the nested lists + +**Usage:** + +```graphql +type Query { + # User must have both scopes + users: [User] @requiresScopes(scopes: [["read:users", "write:users"]]) + + # User must have either scope + admin: AdminPanel @requiresScopes(scopes: [["admin:users"], ["admin:system"]]) + + # User must have read:users AND (admin:users OR admin:system) + reports: [Report] @requiresScopes(scopes: [["read:users", "admin:users"], ["read:users", "admin:system"]]) +} +``` + +## Configuration Options + +### `enabled` + +- **Type:** `boolean` +- **Default:** `true` + +Whether to enable authorization directives processing. Set to `false` to disable authorization checks entirely. + +### `unauthorized.mode` + +- **Type:** `string` +- **Allowed values:** `"filter"` or `"reject"` +- **Default:** `"filter"` + +Controls how the router handles unauthorized field access: + +- `"filter"`: Remove unauthorized fields and continue processing (returns errors for removed fields) +- `"reject"`: Reject the entire request if any unauthorized fields are accessed + +## Examples + +### Filter Mode (Default) + +With `filter` mode, unauthorized fields are silently removed from the operation, but the query +continues to execute and return the data the user can access: + +```yaml filename="router.config.yaml" +authorization: + directives: + enabled: true + unauthorized: + mode: filter +``` + +**Request:** + +```graphql +query { + publicData + me { + name + email + } +} +``` + +If the user is not authenticated, the response might look like: + +```json +{ + "data": { + "publicData": "available", + "me": null + }, + "errors": [ + { + "message": "Unauthorized field or type", + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE", + "affectedPath": "me" + } + } + ] +} +``` + +### Reject Mode + +With `reject` mode, if any field is unauthorized, the entire request is rejected: + +```yaml filename="router.config.yaml" +authorization: + directives: + enabled: true + unauthorized: + mode: reject +``` + +**Request (same as above):** + +```graphql +query { + publicData + me { + name + email + } +} +``` + +**Response:** + +```json +{ + "data": null, + "errors": [ + { + "message": "Unauthorized field or type", + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} +``` diff --git a/packages/web/docs/src/content/router/configuration/index.mdx b/packages/web/docs/src/content/router/configuration/index.mdx index 0090314a06b..fec98ef48f9 100644 --- a/packages/web/docs/src/content/router/configuration/index.mdx +++ b/packages/web/docs/src/content/router/configuration/index.mdx @@ -13,6 +13,7 @@ Some configuration variables can be overridden at runtime using This page covers all the main configuration options available. Each option links to a detailed page that explains how to use that feature. +- [`authorization`](./configuration/authorization): Define field-level access control using directives. - [`cors`](./configuration/cors): Control cross-origin requests to your API. - [`csrf`](./configuration/csrf): Protect against cross-site request forgery attacks. - [`headers`](./configuration/headers): Modify HTTP headers between clients and subgraphs. diff --git a/packages/web/docs/src/content/router/security/_meta.ts b/packages/web/docs/src/content/router/security/_meta.ts index b8b80ada399..cf4b2a616d1 100644 --- a/packages/web/docs/src/content/router/security/_meta.ts +++ b/packages/web/docs/src/content/router/security/_meta.ts @@ -1,4 +1,5 @@ export default { + authorization: 'Authorization', cors: 'Configuring CORS', csrf: 'CSRF Prevention', 'jwt-authentication': 'JWT Authentication', diff --git a/packages/web/docs/src/content/router/security/authorization.mdx b/packages/web/docs/src/content/router/security/authorization.mdx new file mode 100644 index 00000000000..269c2e2c03e --- /dev/null +++ b/packages/web/docs/src/content/router/security/authorization.mdx @@ -0,0 +1,728 @@ +--- +title: 'Authorization' +--- + +# Authorization + +Authorization directives allow you to define fine-grained access control directly in your GraphQL schema. Instead of handling authorization logic in resolvers or middleware, you declare which fields and types require authentication or specific scopes using directives. The router enforces these rules at the router level, ensuring consistent protection across your entire federated graph. + +This guide explains the core concepts and shows you how to implement authorization directives. For the complete configuration reference, see [`authorization` configuration](../configuration/authorization). + +## How Authorization Directives Work + +When a GraphQL request comes to the router, it goes through these steps: + +1. **Request arrives** with user credentials (typically a JWT token) +2. **Router extracts user information** from the token (authentication status and scopes) +3. **Router checks each field** in the requested query against authorization directives +4. **Access is allowed or denied** based on the field's directive requirements and the user's credentials +5. **Response is returned** with either the requested data, errors, or a full rejection + +The key insight is that authorization happens **before** your subgraphs are even called. This protects sensitive fields at the router level. + +## Integration with JWT Authentication + +Authorization directives work alongside your [JWT authentication setup](./jwt-authentication.mdx). Here's the flow: + +1. **Client sends request** with JWT token in the `Authorization` header +2. **Router validates JWT** using your configured JWKS provider +3. **Router extracts scopes** from the JWT claims (`scope` field) +4. **Router checks authorization directives** against the extracted scopes +5. **Query proceeds or fails** based on authorization result + +**Configuration example:** + +```yaml filename="router.config.yaml" +jwt: + require_authentication: false # Allow anonymous requests + jwks_providers: + - source: remote + url: https://your-auth-provider.com/.well-known/jwks.json + +authorization: + directives: + enabled: true + unauthorized: + mode: filter # Or 'reject' for stricter enforcement +``` + +With this setup, your GraphQL API allows both anonymous and authenticated requests, but authorization directives control which fields each user can access. + +## The Two Authorization Directives + +### `@authenticated` + +The `@authenticated` directive marks a field or type as requiring the user to be authenticated. Anonymous requests (without a valid token) cannot access these fields. + +**Example:** + +```graphql +type Query { + # Anyone can search public posts + searchPublicPosts(query: String!): [Post] + + # Only logged-in users can access their drafts + myDraftPosts: [Post] @authenticated + + # Only logged-in users can see their profile + me: User @authenticated +} + +type User { + id: ID! + username: String! + + # Public information + bio: String + + # Private information - requires authentication + email: String @authenticated + notifications: [Notification!] @authenticated +} +``` + +In this example: +- `searchPublicPosts` is accessible to everyone +- `myDraftPosts` requires authentication +- `me` requires authentication +- On `User`, `email` and `notifications` require authentication, but `id`, `username`, and `bio` don't + +### `@requiresScopes` + +The `@requiresScopes` directive provides more granular control by requiring specific scopes. Scopes are permissions granted to a user, typically stored in their JWT token (under `scope` claim as string - separated by space). This is how you implement role-based and permission-based access control. + +**Scope logic:** + +- **Within a single list** (AND logic): User must have ALL scopes + - Example: `@requiresScopes(scopes: [["read:users", "write:users"]])` means the user needs both scopes + +- **Across multiple lists** (OR logic): User must satisfy at least ONE complete list + - Example: `@requiresScopes(scopes: [["read:users"], ["admin"]])` means the user needs either the `read:users` scope OR the `admin` scope + +**Example:** + +```graphql +type Query { + # Anyone can view public users + users: [User] + + # Requires read:users scope + userDetails(id: ID!): User @requiresScopes(scopes: [["read:users"]]) + + # Requires either read:admin OR manage:users scope + allUsers: [User] @requiresScopes(scopes: [["read:admin"], ["manage:users"]]) + + # Requires both admin scope AND billing:read scope + billingReport: String @requiresScopes(scopes: [["admin", "billing:read"]]) +} + +type Mutation { + # Requires write:users scope + updateUser(id: ID!, input: UserInput!): User + @requiresScopes(scopes: [["write:users"]]) + + # Requires admin scope + deleteUser(id: ID!): Boolean + @requiresScopes(scopes: [["admin"]]) + + # Requires delete:orders scope + deleteOrder(id: ID!): Boolean + @requiresScopes(scopes: [["delete:orders"]]) +} + +type User { + id: ID! + username: String! + + # Public information - no restriction + bio: String + + # Private information - requires read:email scope + email: String @requiresScopes(scopes: [["read:email"]]) + + # Admin-only information - requires admin scope + internalNotes: String @requiresScopes(scopes: [["admin"]]) + + # Requires either admin scope OR support:user:read scope + supportTickets: [SupportTicket!] + @requiresScopes(scopes: [["admin"], ["support:user:read"]]) +} +``` + +## Combining Directives Across Types + +When a type is defined across multiple subgraphs (federation), authorization requirements are combined using logical `AND`. This means a user must satisfy all requirements from all subgraphs to access that type. + +**Example:** + +Imagine a `Product` type that exists in multiple services: + +`inventory` subgraph: +```graphql +type Product @key(fields: "id") @authenticated { + id: ID! + sku: String! + inStock: Int +} +``` + +`pricing` subgraph: +```graphql +type Product @key(fields: "id") @requiresScopes(scopes: [["pricing:read"]]) { + id: ID! + price: Float + discounts: [Discount!] +} +``` + +**Resulting requirement:** To access any `Product` field, a user must be: +- `@authenticated` (from inventory subgraph) +- Have `pricing:read` scope (from pricing subgraph) + +So querying `product.inStock` requires both authentication and the pricing scope. + +## Type-Level vs Field-Level Directives + +Authorization can be applied at two levels: + +### Type-Level Protection + +When you put a directive on a type, it protects ALL fields of that type by default: + +```graphql +type AdminPanel @authenticated { + users: [User!] + logs: [String!] + settings: SystemSettings +} +``` + +Any request trying to access `users`, `logs`, or `settings` must be authenticated. + +### Field-Level Protection + +When you put a directive on a specific field, it adds additional restrictions beyond any type-level protection: + +```graphql +type User @authenticated { + id: ID! + username: String! + + # This field requires authentication (from type) PLUS email:read scope + email: String @requiresScopes(scopes: [["email:read"]]) + + # This field requires only authentication (from type) + name: String +} +``` + +**The key principle:** Field-level requirements are combined with type-level requirements using `AND` logic. The field is more restrictive. + +## Handling Authorization Errors + +The router supports two modes for handling authorization violations: + +### Filter Mode (Default) + +When a user tries to access an unauthorized field in **filter mode**, the router removes that field from the response but continues processing the rest of the query. An error is added for the removed field, but the query doesn't completely fail. + +```graphql +# User Query (user has authentication but not admin scope) +query { + dashboard { + publicMetrics + adminPanel # User not authorized + } +} +``` + +**Response:** +```json +{ + "data": { + "dashboard": { + "publicMetrics": { ... }, + "adminPanel": null + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE", + } + } + ] +} +``` + +### Reject Mode + +In **reject mode**, if a user tries to access any unauthorized field, the entire request is rejected. No data is returned, only an error. + +```graphql +# Same query as above, but with reject mode enabled +query { + dashboard { + publicMetrics + adminPanel # User not authorized + } +} +``` + +**Response:** +```json +{ + "data": null, + "errors": [ + { + "message": "Unauthorized field or type", + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE", + } + } + ] +} +``` + + +## Specification + +### 1. Overview + +This document specifies the behavior and application of authorization directives. +It defines the rules for applying these directives to various type definitions to ensure consistent and predictable security enforcement across all subgraphs. + +### 2. Authorization Directives + +The primary authorization directives are: + +| `@authenticated` | Requires the requesting client to be authenticated. | +| --- | --- | +| `@requiresScopes` | Requires the authenticated client's access token to possess a specific set of scopes. | +| `@policy` | Requires the request to satisfy a specific authorization policy, evaluated by the gateway. The logic is provided by the user, typically as a gateway plugin. | + +Collectively, these are referred to as "auth directives". + +### 3. General Principles + +#### 3.1. Composition Logic + +When auth directives are applied at multiple levels (e.g., on a type and its field) or across different subgraphs for the same type, their requirements are combined using a logical `AND`. +A request must satisfy the combined set of all applicable authorization policies to access the protected resource. + +#### 3.2. Scope of Protection + +Auth directives protect **output types**. They are applied when a field, type, enum, or scalar is being returned in a response. +They do not apply to their usage in input types (e.g., arguments), as this constitutes data provided by the client. + +#### 3.3 Merging and Normalizing `@requiresScopes` + +When an entity (like a type or field) is protected by multiple `@requiresScopes` directives, typically from different subgraphs, the resulting policy is the logical `AND` of all individual scope policies. The composition must compute a single, normalized `scopes` argument that represents this combined requirement. + +For clarity, remember the logic of the `scopes` argument: + +- The outer list represents a logical `OR`. +- The inner list of scopes represents a logical `AND`. +- Example: `scopes: [["user:read", "user:email"], ["admin"]]` means the client must have (`user:read` AND `user:email`) OR (`admin`). + +##### 3.3.1. The Merging Algorithm + +The process involves two main steps: creating a combined set of scope groups, and then pruning redundant groups from the result. + +**Step 1: Create Combined Groups** + +To satisfy `PolicyA AND PolicyB`, a client must satisfy at least one scope group from `PolicyA` and at least one scope group from `PolicyB`. The new, combined scope groups are formed by taking the **union** of every possible pairing of a scope group from the first policy with a scope group from the second. + +**Step 2: Prune Redundant Groups** + +After generating the combined list, simplify it by removing any group that is a superset of another group in the list. A scope group is considered redundant if another, more permissive group (i.e., a subset with fewer requirements) exists. If a client can satisfy the subset, they can automatically satisfy the superset, making the superset unnecessary to list. + +##### 3.3.2. Example + +Let's apply this algorithm to a `User` type that is defined across an `accounts` service and a `billing` service. + +`accounts` subgraph + +```graphql +@requiresScopes(scopes: + [ + ["user:read", "user:email:read"], + ["admin"] + ] +) + +``` + +Logical meaning + +```graphql +("user:read" AND "user:email:read") + OR +("admin") +``` + +`billing` subgraph + +```graphql +@requiresScopes(scopes: + [ + ["user:read", "billing:read"], + ["admin", "billing:invoice:read"], + ["support:user:read"] + ] +) +``` + +Logical meaning + +```graphql +("user:read" AND "billing:read") + OR +("admin" AND "billing:invoice:read") + OR +("support:user:read") +``` + +Applying the Algorithm: + +**1. Create the Cross-Product:** + +We will pair each of the 2 groups from the `accounts` policy with each of the 3 groups from the `billing` policy, creating 2 * 3 = 6 new combined groups. + +```graphql +["user:read", "user:email:read"] ∪ ["user:read", "billing:read"] + -> ["user:read", "user:email:read", "billing:read"] + +["user:read", "user:email:read"] ∪ ["admin", "billing:invoice:read"] + -> ["user:read", "user:email:read", "admin", "billing:invoice:read"] + +["user:read", "user:email:read"] ∪ ["support:user:read"] + -> ["user:read", "user:email:read", "support:user:read"]` + +["admin"] ∪ ["user:read", "billing:read"] + -> ["admin", "user:read", "billing:read"] + +["admin"] ∪ ["admin", "billing:invoice:read"] + -> ["admin", "billing:invoice:read"] + +["admin"] ∪ ["support:user:read"] + -> ["admin", "support:user:read"] +``` + +**2. Prune Redundant Groups:** + +Now we examine the raw list for groups that are supersets of others. + +The group `["user:read", "user:email:read", "admin", "billing:invoice:read"]` contains all the scopes from `["admin", "billing:invoice:read"]`. Therefore, the first group is a redundant superset and is **removed**. + +The final, merged directive on the global `User` type is: + +```graphql +@requiresScopes(scopes: [ + ["user:read", "user:email:read", "billing:read"], + ["user:read", "user:email:read", "support:user:read"], + ["admin", "user:read", "billing:read"], + ["admin", "billing:invoice:read"], + ["admin", "support:user:read"] +]) +``` + +This deterministic process ensures that the combined policy is both logically correct and expressed in its simplest form. + +### 4. Rules by GraphQL Type + +#### 4.1. Object Types + +The use of auth directives on object types and their fields is **allowed**. + +- **Type-Level Application**: When an auth directive is applied to an object type, it establishes a baseline authorization requirement for accessing any field on that type. +- **Field-Level Application**: Directives on a specific field add to any requirements inherited from the object type. +- **Composition**: The effective authorization policy for a field is the logical `AND` of its own directives and any directives applied to its parent object type. + +##### 4.1.1. Federated Object Type Scenarios + +When an object type is extended across multiple subgraphs, the type-level auth directives from all definitions are combined using `AND` logic to form a global baseline requirement for that type. + +**Scenario: Merging Type-Level Directives** + +Consider a `Product` type defined in an `inventory` subgraph and extended in a `reviews` subgraph. + +`inventory` subgraph + +```graphql +type Product + @key(fields: "upc") + @authenticated +{ + upc: ID! + inStock: Int +} +``` + +`reviews` subgraph + +```graphql +type Product + @key(fields: "upc") + @requiresScopes(scopes: [["product:read"]]) +{ + upc: ID! + reviews: [Review!] +} +``` + +**Resulting Policy:** + +The global `Product` type effectively has `@authenticated AND @requiresScopes(scopes: [["product:read"]])` applied. + +- To query `Product.inStock`, the client must be authenticated **AND** have the `product:read` scope. +- To query `Product.reviews`, the client must also be authenticated **AND** have the `product:read` scope. + +**Scenario: Field-Level Directives on Federated Types** + +Field-level directives are combined with the *globally merged* type-level directives. + +`accounts` subgraph + +```graphql +type User @key(fields: "id") + @authenticated +{ + id: ID! + email: String + @requiresScopes(scopes: [["email:read"]]) +} +``` + +`profiles` subgraph + +```graphql +type User @key(fields: "id") + @policy(policies: [["PublicProfile"]]) +{ + id: ID! + profile: Profile +} +``` + +**Resulting Policy:** + +The global `User` type has a baseline policy of `@authenticated AND @policy(policies: [["PublicProfile"]])`. + +- To query `User.profile`, the client must be authenticated **AND** satisfy the `PublicProfile` policy. +- To query `User.email`, the client must be authenticated, satisfy the `PublicProfile` policy, **AND** have the `email:read` scope. + +#### 4.2. Enums and Scalars + +The use of auth directives on enum and scalar types is **allowed**. + +##### 4.2.1. Federated Enum and Scalar Scenarios + +If multiple subgraphs define the same custom scalar or enum with different auth directives, the requirements are combined globally using `AND` logic. +Any field in the supergraph that returns that type will be protected by the combined policy. + +**Scenario: Merging Scalar Directives** + +Consider a `SensitiveString` scalar defined with different protections in two subgraphs. + +`pii` subgraph + +```graphql +scalar SensitiveString + @requiresScopes(scopes: [["pii:read"]]) +``` + +`compliance` subgraph + +```graphql +scalar SensitiveString + @policy(policies: [["GDPR_Compliant"]]) +``` + +**Resulting Policy:** + +Any field across the entire federated graph that returns a `SensitiveString` will require the client to have the `pii:read` scope **AND** satisfy the `GDPR_Compliant` policy. + +```graphql +# In a third subgraph (e.g., users) +type User { + nationalId: SensitiveString # Accessing this field requires both protections +} + +``` + +#### 4.3. Interface + +The use of auth directives on interface types and its fields is **disallowed**. + +Instead, they are applied to the concrete `type` definitions that implement the interface. +The composition is responsible for computing the effective policy for the interface based on its implementing types. + +**Interface Type Policy** +The effective authorization policy for an interface type is the logical `AND` of the policies of all its implementing object types across the entire federation. + +**Interface Field Policy** +The effective authorization policy for a field on an interface is the logical `AND` of the policies on that same field across all corresponding implementing object types. + +**Rationale** +This aligns with limitations in the `@apollo/subgraph` library and avoids ambiguity in policy enforcement across implementing types. Authorization should be defined on the concrete object types that implement the interface. + +##### 4.3.1. Example Schema + +Consider an `Item` interface implemented by `Book` and `Video` across two subgraphs. We will add a field unique to each implementing type (`author` and `director`) to better illustrate query behavior. + +`books` subgraph + +```graphql +interface Item { + id: ID! + title: String +} + +type Book implements Item + @authenticated +{ + id: ID! + title: String + @requiresScopes(scopes: [["book:read"]]) + author: String + # Inherits @authenticated from the Book type +} +``` + +`videos` subgraph + +```graphql +type Video implements Item + @policy(policies: [["VideoAccess"]]) +{ + id: ID! + title: String + @requiresScopes(scopes: [["video:read"]]) + director: String + @requiresScopes(scopes: [["video:metadata"]]) +} + +``` + +**Resulting Effective Policies on the `Item` Interface:** + +- **`Item` Interface Type**: The effective policy is `@authenticated AND @policy(policies: [["VideoAccess"]])`. +- **`Item.title` Field**: The effective policy is `@requiresScopes(scopes: [["book:read"]]) AND @requiresScopes(scopes: [["video:read"]])`. + +##### 4.3.2. Query Scenarios with Interfaces + +Here is how these policies are applied to different queries. + +**Scenario 1: Querying a Common Interface Field** + +When you query a field directly on the interface, the most restrictive, combined policy is applied. + +```graphql +query GetItemTitle { + item(id: "123") { + title + } +} +``` + +**Authorization Analysis:** + +1. The `item` field returns the `Item` interface, so its type policy (`@authenticated AND @policy(policies: [["VideoAccess"]])`) is checked first. +2. The `title` field is being accessed on the `Item` interface, so its combined field policy (`@requiresScopes(scopes: [["book:read"]]) AND @requiresScopes(scopes: [["video:read"]])`) is checked. +3. **Total Requirement**: The client must be authenticated, satisfy the `VideoAccess` policy, and possess **both** the `book:read` and `video:read` scopes to execute this query successfully, regardless of whether the returned item is a `Book` or a `Video`. + +**Scenario 2: Querying with Inline Fragments** + +When you use inline fragments, authorization is applied based on the concrete type within the fragment. This allows for more granular access. + +```graphql +query GetSpecificItem { + item(id: "123") { + ... on Book { + author + } + ... on Video { + director + } + } +} +``` + +**Authorization Analysis:** + +1. The `item` field's type policy (`@authenticated AND @policy(policies: [["VideoAccess"]])`) is always checked first. +2. The gateway resolves `item(id: "123")` and determines its concrete type. +3. **If the item is a `Book`**: + - The `... on Book` fragment is entered. + - The policy for `Book.author` is checked, which is `@authenticated` (inherited from the `Book` type). + - **Total Requirement**: `@authenticated AND @policy(policies: [["VideoAccess"]])`. +4. **If the item is a `Video`**: + - The `... on Video` fragment is entered. + - The policy for `Video.director` is checked, which is `@requiresScopes(scopes: [["video:metadata"]])`. + - **Total Requirement**: `@authenticated AND @policy(policies: [["VideoAccess"]]) AND @requiresScopes(scopes: [["video:metadata"]])`. + +This demonstrates how inline fragments allow clients to access data for which they are specifically authorized, even if they don't have the superset of permissions required to query all fields on the interface directly. + +**Scenario 3: Querying Both Common and Specific Fields** + +When a query asks for a common field and also uses an inline fragment, all applicable policies are checked. + +```graphql +query GetItemDetails { + item(id: "456") { + title + ... on Book { + author + } + } +} +``` + +**Authorization Analysis:** + +1. The `item` field's type policy is checked first: `@authenticated AND @policy(policies: [][["VideoAccess"]])`. +2. The `title` field's interface-level policy is checked: `@requiresScopes(scopes: [["book:read"]]) AND @requiresScopes(scopes: [["video:read"]])`. +3. The gateway resolves the item's type. If it's a `Book`, the `... on Book` fragment is entered, and the policy for `Book.author` (`@authenticated`) is also confirmed. +4. **Total Requirement**: A client must satisfy the policies from steps 1 and 2 to even attempt this query. If the resolved item is a `Book`, the policies from step 3 are also relevant, but they are already covered by the more restrictive policies from the preceding steps. + +#### 4.4. Unions + +The use of auth directives on union types is **prohibited**. + +**Rationale**: Union types do not have fields and cannot be queried directly. Authorization policies should be placed on the concrete object types that constitute the union's possible members. + +### 5. Field-level Dependencies (`@requires`) + +A field utilizing the `@requires` directive to access fields from another entity must define an authorization policy that is a superset of the policies on all the required fields. +This ensures that a federated query does not create a loophole to bypass the security policies of the underlying fields. + +Example + +Assume the `products` subgraph defines `Product.price`. + +```graphql +type Product @key(fields: "id") { + id: ID! + price: Float @requiresScopes(scopes: [["read:price"]]) +} +``` + +In the `reviews` subgraph, the `Review` type must ensure its own authorization accommodates the `read:price` requirement if it needs to access `Product.price`. + +```graphql +type Product @key(fields: "id") { + id: ID! + price: Float @external +} + +type Review { + body: String + # This field's policy MUST be a superset of Product.price's policy. + # The following is valid because it requires the necessary scope. + productPrice: Float + @requires(fields: "product { price }") + @requiresScopes(scopes: [["read:price"]]) +} +```