On this page

Custom Authorization Policies

The @policy directive lets you enforce authorization rules that the router cannot evaluate on its own, such as resource ownership, tenant isolation, or any business rule that lives in your own systems. Instead of hard-coding that logic in every subgraph, you declare a policy name in your schema and let a coprocessor decide whether the current request satisfies it.

@policy complements @authenticated and @requiresScopes, which are described in the general Authorization guide. Use @policy when the decision cannot be derived from a JWT scopes alone.

For the complete configuration reference, see authorization configuration.

How It Works

  1. Collect - before calling the graphql.analysis coprocessor stage, the router walks the requested operation and publishes every @policy policy it depends on to the hive::authorization::required_policies request context key, mapped to null.
  2. Decide - your coprocessor (or a plugin) looks at the request and overwrites each entry with true or false.
  3. Enforce - after the stage returns, the router applies the decisions. Any policy left null, or missing from the answer entirely, is treated as denied. Unauthorized fields are then handled exactly like @authenticated/@requiresScopes violations, following your configured authorization.directives.unauthorized.mode.

Defining Policies in Your Schema

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@policy"])

type Query {
  users: [User] @policy(policies: [["admin"], ["read_users", "internal"]])
}

type Product {
  upc: String!
  # Requires the "read_inventory" policy to be granted
  inStock: Boolean @policy(policies: [["read_inventory"]])
}

Just like @requiresScopes, policies is a list of lists: an OR of AND groups.

  • Single list (AND logic): every policy in the list must be granted.
  • Multiple lists (OR logic): at least one full list must be granted.

In the example above, users is allowed if the admin policy is granted, or if both read_users and internal are granted.

When multiple directives protect the same field, all of them must be satisfied - @policy is combined with @authenticated/@requiresScopes using AND, the same way multiple auth directives on one field already compose in the Authorization guide.

Wiring Up a Coprocessor

Enable the graphql.analysis stage and include the request context, so your coprocessor can read hive::authorization::required_policies and reply with its decisions:

router.config.yaml
coprocessor:
  url: http://127.0.0.1:8081/coprocessor
  protocol: http1
  stages:
    graphql:
      analysis:
        include:
          context: true

authorization:
  directives:
    enabled: true
    unauthorized:
      mode: filter # Or 'reject'

Coprocessor Input

For an operation that selects Query.users, the graphql.analysis stage payload includes the policies the router needs a decision on, each initialized to null:

graphql.analysis request payload
{
  "version": 1,
  "stage": "graphql.analysis",
  "control": "continue",
  "id": "...",
  "context": {
    "hive::authorization::required_policies": {
      "admin": null,
      "read_users": null,
      "internal": null
    }
  }
}

Coprocessor Output

Your coprocessor looks up the current user (from headers, a session store, an internal service - whatever your business logic requires) and answers by overwriting the relevant entries:

graphql.analysis response
{
  "version": 1,
  "control": "continue",
  "context": {
    "hive::authorization::required_policies": {
      "admin": false,
      "read_users": true,
      "internal": true
    }
  }
}

Given this answer, the read_users AND internal group is fully satisfied, so users is authorized even though admin was denied.

Handling Denied Policies

A denied policy is handled exactly like any other unauthorized field access - the field is filtered out (or the whole request is rejected in reject mode) and an error is returned:

GraphQL response, 'filter' mode
{
  "data": {
    "users": null
  },
  "errors": [
    {
      "message": "Unauthorized field or type",
      "extensions": {
        "code": "UNAUTHORIZED_FIELD_OR_TYPE",
        "affectedPath": "users"
      }
    }
  ]
}

See Handling Authorization Errors for the full behavior of filter and reject modes.