From f22213e2b4361030217b519a30d86dae1098ec63 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 25 Aug 2023 10:16:56 +0200 Subject: [PATCH 01/16] Reintroduce authorization documentation This reverts commit c4f1a4d323e12901c74db0c1d836d31e5e77a8ca. --- .../feat_geal_authorization_directives.md | 22 + .../maint_geal_authorization_analytics.md | 9 + docs/source/config.json | 7 + docs/source/configuration/authorization.mdx | 604 ++++++++++++++++++ docs/source/enterprise-features.mdx | 1 + 5 files changed, 643 insertions(+) create mode 100644 .changesets/feat_geal_authorization_directives.md create mode 100644 .changesets/maint_geal_authorization_analytics.md create mode 100644 docs/source/configuration/authorization.mdx diff --git a/.changesets/feat_geal_authorization_directives.md b/.changesets/feat_geal_authorization_directives.md new file mode 100644 index 0000000000..5984c85d48 --- /dev/null +++ b/.changesets/feat_geal_authorization_directives.md @@ -0,0 +1,22 @@ +### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397), [PR #3662](https://github.com/apollographql/router/pull/3662)) + +We introduce two new directives, `@authenticated` and `requiresScopes`, that define authorization policies for field and types in the supergraph schema. + +They are defined as follows: + +```graphql +directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + +scalar federation__Scope +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + +They are implemented by hooking the request lifecycle at multiple steps: +- in query analysis, we extract from the query the list of scopes that would be relevant to authorize the query +- in a supergraph plugin, we calculate the authorization status and put it in the context: `is_authenticated` for `@authenticated`, and the intersection of the query's required scopes and the scopes provided in the token, for `@requiresScopes` +- in the query planning phase, we filter the query to remove the fields that are not authorized, then the filtered query goes through query planning +- at the subgraph level, if query deduplication is active, the authorization status is used to group queries together +- at the execution service level, the response is formatted according to the filtered query first, which will remove any unauthorized information, then to the shape of the original query, which will propagate nulls as needed +- at the execution service level, errors are added to the response indicating which fields were removed because they were not authorized + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397 https://github.com/apollographql/router/pull/3662 \ No newline at end of file diff --git a/.changesets/maint_geal_authorization_analytics.md b/.changesets/maint_geal_authorization_analytics.md new file mode 100644 index 0000000000..97af5f6012 --- /dev/null +++ b/.changesets/maint_geal_authorization_analytics.md @@ -0,0 +1,9 @@ +### add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) + +The new metrics, for use in Router Analytics, is a counter called `apollo.router.operations.authorization` +and contains the following boolean attributes: +- filtered: some fields were filtered from the query +- authenticated: the query uses fields or types tagged with the `@authenticated` directive +- requires_scopes: the query uses fields or types tagged with the `@requiresScopes` directive + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3660 \ No newline at end of file diff --git a/docs/source/config.json b/docs/source/config.json index 74a3cfbd31..28f2d33a7d 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -42,6 +42,13 @@ "enterprise" ] ], + "Authorization": [ + "/configuration/authorization", + [ + "enterprise", + "experimental" + ] + ], "Subgraph Authentication": "/configuration/authn-subgraph", "Operation limits": [ "/configuration/operation-limits", diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx new file mode 100644 index 0000000000..8651fa67fa --- /dev/null +++ b/docs/source/configuration/authorization.mdx @@ -0,0 +1,604 @@ +--- +title: Authorization in the Apollo Router +description: Strengthen service security with a centralized governance layer +minVersion: 1.27.0 +--- + + + + + +APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal services, checks can be essential to limit data to authorized parties. + +Services may have their own access controls, but enforcing authorization _in the Apollo Router_ is valuable for a few reasons: + +- **Optimal query execution**: Validating authorization _before_ processing requests allows for early request termination. Stopping unauthorized requests at the edge of your graph reduces the load on your services and enhances performance. + + ```mermaid + flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Users
API]; + serviceC[Posts
API]; + end + router -.->|"❌ Subquery"| serviceB & serviceC; + clients -->|"⚠️Unauthorized
request"| router; + ``` + + - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. Check out [How it works](#how-it-works) to learn more. + + ```mermaid + flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Users
API]; + serviceC[Posts
API]; + end + router -->|"✅ Authorized
subquery"| serviceC; + router -.->|"❌ Unauthorized
subquery"| serviceB; + clients -->|"⚠️ Partially authorized
request"| router; + ``` + - [Query deduplication](./traffic-shaping/#query-deduplication) also accounts for authorization by grouping requested fields based on their required authorization. Entire groups can be eliminated from the query plan if they don't have the correct authorization. + +- **Declarative access rules**: You define access controls at the field level, and GraphOS [composes](#composition-and-federation) them across your services. These rules create graph-native governance without the need for an extra orchestration layer. + +- **Principled architecture**: Through composition, the router centralizes authorization logic while allowing for auditing at the service level. This centralized authorization is an initial checkpoint that other service layers can reinforce. + + ```mermaid + flowchart LR; + clients(Clients); + Level2:::padding + subgraph Level1["
🔐 Router layer                                                   "] + router(["Apollo Router"]); + subgraph Level2["🔐 Service layer"] + serviceB[Users
API]; + serviceC[Posts
API]; + end + end + + router -->|"Subquery"| serviceB & serviceC; + clients -->|"Request"| router; + + classDef padding padding-left:1em, padding-right:1em + ``` + +## How it works + +The Apollo Router provides access controls via **authorization directives**. Using the `@requiresScopes` and `@authenticated` directives, you can define access to specific fields and types across your supergraph: + +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. + +For example, imagine you're building a social media platform that includes a `Users` subgraph. You can use the [`@requiresScopes`](#requiresscopes) directive to declare that viewing other users' information requires the `read:user` scope: + +```graphql +type Query { + users: [User!]! @requiresScopes(scopes: [["read:users"]]) +} +``` + +You can use the [`@authenticated`](#authenticated) directive to declare that users must be logged in to update their own information: + +```graphql +type Mutation { + updateUser(input: UpdateUserInput!): User! @authenticated +} +``` + +You can define both directives—together or separately—at the field level to fine-tune your access controls. +GraphOS [composes](#composition-and-federation) restrictions into the supergraph schema so that each subgraph's restrictions are respected. +The router then enforces these directives on all incoming requests. + +## Prerequisites + +The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. + +To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. + +- **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. +- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. +- **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. + +## Authorization directives + +Authorization directives are enabled in the configuration through the following option: + +```yaml title="router.yaml" +authorization: + preview_directives: + enabled: true +``` + +### `@requiresScopes` + +The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +To declare which scopes are required, the directive should include a `scopes` argument with an array of the required scopes. + +```graphql +@requiresScopes(scopes: [["scope1", "scope2", "scope3"]]) +``` + +Depending on the scopes present on the request, the router filters out unauthorized fields and types. + +> You can use Boolean logic to define the required scopes. See [Combining required scopes](#combining-required-scopes-with-andor-logic) for details. + +The directive validates the required scopes by loading the claims object at the `apollo_authentication::JWT::claims` key in a request's context. +The claims object's `scope` key's value should be a space-separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + +```rhai +claims = context["apollo_authentication::JWT::claims"] +claims["scope"] = "scope1 scope2 scope3" +``` + + + +If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). + +The example below extracts an array of scopes from the `"roles"` claim and reformats them as a space-separated string. + +```Rhai +fn router_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + let roles = claims["roles"]; + + let scope = ""; + if roles.len() > 1 { + scope = roles[0]; + } + + if roles.len() > 2 { + for role in roles[1..] { + scope += ' '; + scope += role; + } + } + + claims["scope"] = scope; + request.context["apollo_authentication::JWT::claims"] = claims; + }; + service.map_request(request_callback); +} +``` + + + +#### Usage + +To use the `@requiresScopes` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: + +```graphql +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.5", + import: [..., "@requiresScopes"]) +``` + +#### Combining required scopes with `AND`/`OR` logic + +A request must include _all_ elements in the inner-level `scopes` array to resolve the associated field or type. In other words, the authorization validation uses **AND** logic between the elements in the inner-level `scopes` array. + +```graphql +@requiresScopes(scopes: [["scope1", "scope2", "scope3"]]) +``` + +For the preceding example, a request would need `scope1` **AND** `scope2` **AND** `scope3` to be authorized. + +You can use nested arrays to introduce **OR** logic: + +```graphql +@requiresScopes(scopes: [["scope1"], ["scope2"], ["scope3"]]) +``` + +For the preceding example, a request would need `scope1` **OR** `scope2` **OR** `scope3` to be authorized. + +You can nest arrays and elements as needed to achieve your desired logic. For example: + +```graphql +@requiresScopes(scopes: [["scope1", "scope2"], ["scope3"]]) +``` + +This syntax requires requests to have either (`scope1` **AND** `scope2`) **OR** just `scope3` to be authorized. + + +#### Example `@requiresScopes` use case + +Imagine the social media platform you're building lets users view other users' information only if they have the required permissions. +Your schema may look something like this: + +```graphql +type Query { + user(id: ID!): User @requiresScopes(scopes: [["read:others"]]) + users: [User!]! @requiresScopes(scopes: [["read:others"]]) + post(id: ID!): Post +} + +type User { + id: ID! + username: String + email: String @requiresScopes(scopes: [["read:email"]]) + profileImage: String + posts: [Post!]! +} + +type Post { + id: ID! + author: User! + title: String! + content: String! +} +``` + +Depending on a request's attached scopes, the router executes the following query differently. +If the request includes only the `read:others` scope, then the router executes the following filtered query: + + + +```graphql title="Raw query to router" +query { + users { + username + profileImage + email + } +} +``` + +```graphql title="Scopes: 'read:others'" +query { + users { + username + profileImage + } +} +``` + + + +The response would include an error at the `/users/@/email` path since that field requires the `read:emails` scope. +The router can execute the entire query successfully if the request includes the `read:others read:emails` scope set. + +### `@authenticated` + +The `@authenticated` directive marks specific fields and types as requiring authentication. +It works by checking for the `apollo_authentication::JWT::claims` key in a request's context, that is added either by the JWT authentication plugin, when the request contains a valid JWT, or by an authentication coprocessor. +If the key exists, it means the request is authenticated, and the router executes the query in its entirety. +If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. + +#### Usage + +To use the `@authenticated` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: + +```graphql +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.5", + import: [..., "@authenticated"]) +``` + +#### Example `@authenticated` use case + +Diving deeper into the social media example: let's say unauthenticated users can view a post's title, author, and content. +However, you only want authenticated users to see the number of views a post has received. +You also need to be able to query for an authenticated user's information. + +The relevant part of your schema may look something like this: + +```graphql +type Query { + me: User @authenticated + post(id: ID!): Post +} + +type User { + id: ID! + username: String + email: String @requiresScopes(scopes: ["read:email"]) + posts: [Post!]! +} + +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated +} + +``` + +Consider the following query: + +```graphql title="Sample query" +query { + me { + username + } + post(id: "1234") { + title + views + } +} +``` + +The router would execute the entire query for an authenticated request. +For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. + + + +```graphql title="Query executed for an authenticated request" +query { + me { + username + } + post(id: "1234") { + title + views + } +} +``` + +```graphql title="Query executed for an unauthenticated request" +query { + post(id: "1234") { + title + } +} +``` + + + +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the views for the post with `id: "1234"`. +The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). + +```json title="Unauthenticated request response" +{ + "data": { + "me": null, + "post": { + "title": "Securing supergraphs", + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "me" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + }, + { + "message": "Unauthorized field or type", + "path": [ + "post", + "views" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} +``` + +If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. + +## Composition and federation + +GraphOS's composition strategy for authorization directives is intentionally accumulative. When you define authorization directives on fields and types in subgraphs, GraphOS composes them into the supergraph schema. In other words, if subgraph fields or types include `@requiresScopes` or `@authenticated` directives, they are set on the supergraph too. + +#### Composition with `AND`/`OR` logic + +If shared subgraph fields include multiple directives, composition merges them. For example, suppose the `me` query requires `@authentication` in one subgraph: + + +```graphql title="Subgraph A" +type Query { + me: User @authenticated +} + +type User { + id: ID! + username: String + email: String +} +``` + +and the `read:user` scope in another subgraph: + +```graphql title="Subgraph B" +type Query { + me: User @requiresScopes(scopes: [["read:user"]]) +} + +type User { + id: ID! + username: String + email: String +} +``` + +A request would need to both be authenticated **AND** have the required scope. Recall that the `@authenticated` directive only checks for the existence of the `apollo_authentication::JWT::claims` key in a request's context, so authentication is guaranteed if the request includes scopes. + +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges them with the same logic used to [combine scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic). For example, if one subgraph requires the `read:others` scope on the `users` query: + +```graphql title="Subgraph A" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others"]]) +} +``` + +and another subgraph requires the `read:profiles` scope on `users` query: + +```graphql title="Subgraph B" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:profiles"]]) +} +``` + +Then the supergraph schema would require _both_ scopes for it. + +```graphql title="Supergraph" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others", "read:profiles"]]) +} +``` + +As with [combining scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic), you can use nested arrays to introduce **OR** logic: + +```graphql title="Subgraph A" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"]]) +} +``` + +```graphql title="Subgraph B" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:profiles"]]) +} +``` + +Since both `scopes` arrays are nested arrays, they would be composed using **OR** logic into the supergraph schema: + +```graphql title="Supergraph" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"], ["read:profiles"]]) +} +``` + +This syntax means a request needs either (`read:others` **AND** `read:users`) scopes **OR** just the `read:profiles` scope to be authorized. + +### Authorization and `@key` fields + +The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. +If you use authorization directives on fields defined in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still uses those fields to compose entities between the subgraphs, but the client cannot query them directly. + +Consider these example subgraph schemas: + +```graphql title="Product subgraph" +type Query { + product: Product +} + +type Product @key(fields: "id") { + id: ID! @authenticated + name: String! + price: Int @authenticated +} +``` + +```graphql title="Inventory subgraph" +type Query { + product: Product +} + +type Product @key(fields: "id") { + id: ID! @authenticated + inStock: Boolean! +} +``` + +An unauthenticated request would successfully execute this query: + +```graphql +query { + product { + name + inStock + } +} +``` + +Specifically, under the hood, the router would use the `id` field to resolve the `Product` entity, but it wouldn't return it. + +For the following query, an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. + +```graphql +query { + product { + id + username + } +} +``` + +This behavior resembles what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). + +### Authorization and interfaces + +If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. + +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post`, does: + +```graphql +type Query { + posts: [Post!]! +} + +type User { + id: ID! + username: String + posts: [Post!]! +} + +interface Post { + id: ID! + author: User! + title: String! + content: String! +} + +type PrivateBlog implements Post @authenticated { + id: ID! + author: User! + title: String! + content: String! + publishAt: String + allowedViewers: [User!]! +} +``` + +If an unauthenticated request were to make this query: + +```graphql +query { + posts { + id + author + title + ... on PrivateBlog { + allowedViewers + } + } +} +``` + +The router would filter the query as follows: + +```graphql +query { + posts { + id + author + title + } +} +``` + +The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. + +## Query deduplication + +You can enable [query deduplication](../configuration/traffic-shaping/#query-deduplication) in the router to reduce redundant requests to a subgraph. The router does this by buffering similar queries and reusing the result. + +**Query deduplication takes authorization into account.** First, the router groups unauthenticated queries together. Then it groups authenticated queries by their required scope set. It uses these groups to execute queries efficiently when fulfilling requests. + +## Introspection + +Introspection is turned off in the router by default, [as is best production practice](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/). If you've chosen to [enable it](./overview/#introspection), keep in mind that **authorization directives don't affect introspection**. All fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure it hasn't been enabled in your router configuration. + +With introspection turned off, you can use GraphOS's [schema registry](/graphos/delivery/) to explore your supergraph schema and empower your teammates to do the same. If you want to completely remove fields from a graph rather than just preventing access (even with introspection on), consider building a [contract graph](/graphos/delivery/contracts/). diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index 5594b52c39..38f3028c41 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -11,6 +11,7 @@ The Apollo Router provides expanded performance, security, and customization fea - **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/) - **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/) +- **Access control** to specific fields and types through the [`@authenticated`](./configuration/authorization#authenticated) and [`@requiresScopes`](./configuration/authorization#requiresscopes) directives - Redis-backed [**distributed caching** of query plans and persisted queries](./configuration/distributed-caching/) - **Custom request handling** in any language via [external coprocessing](./customizations/coprocessor/) - **Mitigation of potentially malicious requests** via [operation limits](./configuration/operation-limits) and [safelisting with persisted queries](./configuration/persisted-queries) From efe42298e2cedda499aa81c4181112af789ee1fb Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 25 Aug 2023 08:30:02 -0600 Subject: [PATCH 02/16] Bump version --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8651fa67fa..88c97e3444 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,7 +1,7 @@ --- title: Authorization in the Apollo Router description: Strengthen service security with a centralized governance layer -minVersion: 1.27.0 +minVersion: 1.28.0 --- From 30e65886b7a2aa3388e1b89ef561baeb8c65f8b3 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 25 Aug 2023 09:10:05 -0600 Subject: [PATCH 03/16] Bump --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 88c97e3444..48beb79feb 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,7 +1,7 @@ --- title: Authorization in the Apollo Router description: Strengthen service security with a centralized governance layer -minVersion: 1.28.0 +minVersion: 1.28.1 --- From 4d62f10a85af2e019d59c80c6e623244dcef00b2 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 28 Aug 2023 11:33:00 -0600 Subject: [PATCH 04/16] Add router config --- docs/source/configuration/authorization.mdx | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 48beb79feb..d98aff5a0f 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,7 +6,7 @@ minVersion: 1.28.1 - + APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal services, checks can be essential to limit data to authorized parties. @@ -93,7 +93,13 @@ The router then enforces these directives on all incoming requests. ## Prerequisites -The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. +You must complete two prerequisites before using authorization directives in your subgraph schemas: +- Include **claims** in requests to the router + - Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. +- Configure your router + - You need to add a few lines to your [router's configuration](./overview/) for it to recognize the authorization directives. + +### Including claims in requests To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. @@ -101,6 +107,23 @@ To provide the router with the claims it needs to evaluate authorization directi - **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. - **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. +### Router configuration + +To configure your router to validate authorization directives, follow these steps: + +1. Ensure your router instances are ready to work with PQLs: + - Make sure you're using version `1.28.1` or later of the Apollo Router. + - Make sure your router instances are [connected to your GraphOS Enterprise organization](/graphos/enterprise-features/#enabling-enterprise-features). + +2. Update your router's [YAML config file](./overview/) to include the following: + +```yaml title="router.yaml" +authorization: + preview_directives: + enabled: true +``` +> During [preview](/resources/product-launch-stages#preview), authorization directives are not active by default. This will change once the feature is generally available. + ## Authorization directives Authorization directives are enabled in the configuration through the following option: From ccac9cfd969e427e0c9dc93aca36453754c37152 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 28 Aug 2023 11:49:15 -0600 Subject: [PATCH 05/16] Copy edit --- docs/source/configuration/authorization.mdx | 29 +++------------------ 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index d98aff5a0f..a91040ac1d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,7 +6,7 @@ minVersion: 1.28.1 - + APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal services, checks can be essential to limit data to authorized parties. @@ -93,13 +93,7 @@ The router then enforces these directives on all incoming requests. ## Prerequisites -You must complete two prerequisites before using authorization directives in your subgraph schemas: -- Include **claims** in requests to the router - - Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. -- Configure your router - - You need to add a few lines to your [router's configuration](./overview/) for it to recognize the authorization directives. - -### Including claims in requests +The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. @@ -107,26 +101,9 @@ To provide the router with the claims it needs to evaluate authorization directi - **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. - **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. -### Router configuration - -To configure your router to validate authorization directives, follow these steps: - -1. Ensure your router instances are ready to work with PQLs: - - Make sure you're using version `1.28.1` or later of the Apollo Router. - - Make sure your router instances are [connected to your GraphOS Enterprise organization](/graphos/enterprise-features/#enabling-enterprise-features). - -2. Update your router's [YAML config file](./overview/) to include the following: - -```yaml title="router.yaml" -authorization: - preview_directives: - enabled: true -``` -> During [preview](/resources/product-launch-stages#preview), authorization directives are not active by default. This will change once the feature is generally available. - ## Authorization directives -Authorization directives are enabled in the configuration through the following option: +To use authorization directives in your subgraph schemas, you first need to enable them in your router. To do so, include the following in your router's [YAML config file](./overview/): ```yaml title="router.yaml" authorization: From 2912db8e9a897237628a802742967d1ff6d8f99e Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 28 Aug 2023 12:07:38 -0600 Subject: [PATCH 06/16] Reorder requireScopes before authenticated consistently --- .../feat_geal_authorization_directives.md | 22 ++++++++++--------- .../maint_geal_authorization_analytics.md | 7 +++--- docs/source/enterprise-features.mdx | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.changesets/feat_geal_authorization_directives.md b/.changesets/feat_geal_authorization_directives.md index 5984c85d48..453076cdc9 100644 --- a/.changesets/feat_geal_authorization_directives.md +++ b/.changesets/feat_geal_authorization_directives.md @@ -1,22 +1,24 @@ ### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397), [PR #3662](https://github.com/apollographql/router/pull/3662)) -We introduce two new directives, `@authenticated` and `requiresScopes`, that define authorization policies for field and types in the supergraph schema. +We introduce two new directives, `requiresScopes` and `@authenticated`, that define authorization policies for fields and types in the supergraph schema. They are defined as follows: ```graphql -directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - scalar federation__Scope directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + +directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -They are implemented by hooking the request lifecycle at multiple steps: -- in query analysis, we extract from the query the list of scopes that would be relevant to authorize the query -- in a supergraph plugin, we calculate the authorization status and put it in the context: `is_authenticated` for `@authenticated`, and the intersection of the query's required scopes and the scopes provided in the token, for `@requiresScopes` -- in the query planning phase, we filter the query to remove the fields that are not authorized, then the filtered query goes through query planning -- at the subgraph level, if query deduplication is active, the authorization status is used to group queries together -- at the execution service level, the response is formatted according to the filtered query first, which will remove any unauthorized information, then to the shape of the original query, which will propagate nulls as needed -- at the execution service level, errors are added to the response indicating which fields were removed because they were not authorized +The implementation hooks into the request lifecycle at multiple steps: +- In query analysis, we extract the list of scopes necessary to authorize the query. +- In a supergraph plugin, we calculate the authorization status and put it in the request context: + - for `@requiresScopes`, this is the intersection of the query's required scopes and the scopes provided in the request token + - for `@authenticated`, it is `is_authenticated` or not +- In the query planning phase, we filter the query to remove unauthorized fields before proceeding with query planning. +- At the subgraph level, if query deduplication is active, the authorization status is used to group queries together. +- At the execution service level, the response is first formatted according to the filtered query, which removed any unauthorized information, then to the shape of the original query, which propagates nulls as needed. +- At the execution service level, errors are added to the response indicating which fields were removed because they were not authorized. By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397 https://github.com/apollographql/router/pull/3662 \ No newline at end of file diff --git a/.changesets/maint_geal_authorization_analytics.md b/.changesets/maint_geal_authorization_analytics.md index 97af5f6012..0da7ef5a03 100644 --- a/.changesets/maint_geal_authorization_analytics.md +++ b/.changesets/maint_geal_authorization_analytics.md @@ -1,9 +1,8 @@ -### add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) +### Add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) -The new metrics, for use in Router Analytics, is a counter called `apollo.router.operations.authorization` -and contains the following boolean attributes: +The new metric is a counter called `apollo.router.operations.authorization` and contains the following boolean attributes: - filtered: some fields were filtered from the query -- authenticated: the query uses fields or types tagged with the `@authenticated` directive - requires_scopes: the query uses fields or types tagged with the `@requiresScopes` directive +- authenticated: the query uses fields or types tagged with the `@authenticated` directive By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3660 \ No newline at end of file diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index 38f3028c41..0b7d62776a 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -11,7 +11,7 @@ The Apollo Router provides expanded performance, security, and customization fea - **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/) - **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/) -- **Access control** to specific fields and types through the [`@authenticated`](./configuration/authorization#authenticated) and [`@requiresScopes`](./configuration/authorization#requiresscopes) directives +- **Access control** to specific fields and types through the [`@requiresScopes`](./configuration/authorization#requiresscopes) and [`@authenticated`](./configuration/authorization#authenticated) directives - Redis-backed [**distributed caching** of query plans and persisted queries](./configuration/distributed-caching/) - **Custom request handling** in any language via [external coprocessing](./customizations/coprocessor/) - **Mitigation of potentially malicious requests** via [operation limits](./configuration/operation-limits) and [safelisting with persisted queries](./configuration/persisted-queries) From 96165d1af48fc731874e1889689a479c5affd34d Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 29 Aug 2023 10:13:29 -0600 Subject: [PATCH 07/16] Update default router config --- docs/source/configuration/authorization.mdx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index a91040ac1d..dd6d8e9ca6 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -103,12 +103,16 @@ To provide the router with the claims it needs to evaluate authorization directi ## Authorization directives -To use authorization directives in your subgraph schemas, you first need to enable them in your router. To do so, include the following in your router's [YAML config file](./overview/): +Before using the authorization directives in your subgraph schemas, ensure your router instances are ready to work with them: + - Make sure you're using version `1.28.1` or later of the Apollo Router. + - Make sure your router instances are [connected to your GraphOS Enterprise organization](/graphos/enterprise-features/#enabling-enterprise-features). + +**Authorization directives are enabled in your router by default**. If you want to _disable_ them, include the following in your router's [YAML config file](./overview/): ```yaml title="router.yaml" authorization: preview_directives: - enabled: true + enabled: false ``` ### `@requiresScopes` From c2b6aefc4f135f09db7f18c246dc88037d61728b Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 07:54:32 -0600 Subject: [PATCH 08/16] Apply suggestions from code review Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- .../maint_geal_authorization_analytics.md | 6 +++--- docs/source/configuration/authorization.mdx | 18 ++++++++++-------- docs/source/enterprise-features.mdx | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.changesets/maint_geal_authorization_analytics.md b/.changesets/maint_geal_authorization_analytics.md index 0da7ef5a03..50f235a639 100644 --- a/.changesets/maint_geal_authorization_analytics.md +++ b/.changesets/maint_geal_authorization_analytics.md @@ -1,8 +1,8 @@ ### Add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) The new metric is a counter called `apollo.router.operations.authorization` and contains the following boolean attributes: -- filtered: some fields were filtered from the query -- requires_scopes: the query uses fields or types tagged with the `@requiresScopes` directive -- authenticated: the query uses fields or types tagged with the `@authenticated` directive +- `filtered`: the query has one or more filtered fields +- `requires_scopes`: the query uses fields or types tagged with the `@requiresScopes` directive +- `authenticated`: the query uses fields or types tagged with the `@authenticated` directive By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3660 \ No newline at end of file diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index dd6d8e9ca6..b3f39af6ea 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -12,7 +12,7 @@ APIs provide access to business-critical data. Unrestricted access can result in Services may have their own access controls, but enforcing authorization _in the Apollo Router_ is valuable for a few reasons: -- **Optimal query execution**: Validating authorization _before_ processing requests allows for early request termination. Stopping unauthorized requests at the edge of your graph reduces the load on your services and enhances performance. +- **Optimal query execution**: Validating authorization _before_ processing requests enables the early termination of unauthorized requests. Stopping unauthorized requests at the edge of your graph reduces the load on your services and enhances performance. ```mermaid flowchart LR; @@ -26,7 +26,7 @@ Services may have their own access controls, but enforcing authorization _in the clients -->|"⚠️Unauthorized
request"| router; ``` - - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. Check out [How it works](#how-it-works) to learn more. + - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's personally identifiable information (PII). Check out [How it works](#how-it-works) to learn more. ```mermaid flowchart LR; @@ -40,7 +40,7 @@ Services may have their own access controls, but enforcing authorization _in the router -.->|"❌ Unauthorized
subquery"| serviceB; clients -->|"⚠️ Partially authorized
request"| router; ``` - - [Query deduplication](./traffic-shaping/#query-deduplication) also accounts for authorization by grouping requested fields based on their required authorization. Entire groups can be eliminated from the query plan if they don't have the correct authorization. + - Also, [query deduplication](./traffic-shaping/#query-deduplication) groups requested fields based on their required authorization. Entire groups can be eliminated from the query plan if they don't have the correct authorization. - **Declarative access rules**: You define access controls at the field level, and GraphOS [composes](#composition-and-federation) them across your services. These rules create graph-native governance without the need for an extra orchestration layer. @@ -48,7 +48,7 @@ Services may have their own access controls, but enforcing authorization _in the ```mermaid flowchart LR; - clients(Clients); + clients(Client); Level2:::padding subgraph Level1["
🔐 Router layer                                                   "] router(["Apollo Router"]); @@ -64,7 +64,7 @@ Services may have their own access controls, but enforcing authorization _in the classDef padding padding-left:1em, padding-right:1em ``` -## How it works +## How access control works The Apollo Router provides access controls via **authorization directives**. Using the `@requiresScopes` and `@authenticated` directives, you can define access to specific fields and types across your supergraph: @@ -93,6 +93,8 @@ The router then enforces these directives on all incoming requests. ## Prerequisites +### Configure request claims + The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. @@ -118,7 +120,7 @@ authorization: ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. -To declare which scopes are required, the directive should include a `scopes` argument with an array of the required scopes. +The directive includes a `scopes` argument with an array of the required scopes to declare which scopes are required: ```graphql @requiresScopes(scopes: [["scope1", "scope2", "scope3"]]) @@ -136,7 +138,7 @@ claims = context["apollo_authentication::JWT::claims"] claims["scope"] = "scope1 scope2 scope3" ``` - + If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). @@ -284,7 +286,7 @@ extend schema #### Example `@authenticated` use case -Diving deeper into the social media example: let's say unauthenticated users can view a post's title, author, and content. +Diving deeper into the [social media example](#example-requiresscopes-use-case): let's say unauthenticated users can view a post's title, author, and content. However, you only want authenticated users to see the number of views a post has received. You also need to be able to query for an authenticated user's information. diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index 0b7d62776a..570aaee444 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -11,7 +11,7 @@ The Apollo Router provides expanded performance, security, and customization fea - **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/) - **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/) -- **Access control** to specific fields and types through the [`@requiresScopes`](./configuration/authorization#requiresscopes) and [`@authenticated`](./configuration/authorization#authenticated) directives +- **Access control** of specific fields and types through the [`@requiresScopes`](./configuration/authorization#requiresscopes) and [`@authenticated`](./configuration/authorization#authenticated) directives - Redis-backed [**distributed caching** of query plans and persisted queries](./configuration/distributed-caching/) - **Custom request handling** in any language via [external coprocessing](./customizations/coprocessor/) - **Mitigation of potentially malicious requests** via [operation limits](./configuration/operation-limits) and [safelisting with persisted queries](./configuration/persisted-queries) From 34f53fb241829d46633bd34a3d4af4abf6575f1f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 07:56:13 -0600 Subject: [PATCH 09/16] Apply suggestions from code review Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- docs/source/configuration/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index b3f39af6ea..78f47ebb27 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -66,10 +66,10 @@ Services may have their own access controls, but enforcing authorization _in the ## How access control works -The Apollo Router provides access controls via **authorization directives**. Using the `@requiresScopes` and `@authenticated` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides access controls via **authorization directives** that define access to specific fields and types across your supergraph: - The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. -- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. +- The [`@authenticated`](#authenticated) directive allows access to the annotated field or type for _authenticated requests only_. For example, imagine you're building a social media platform that includes a `Users` subgraph. You can use the [`@requiresScopes`](#requiresscopes) directive to declare that viewing other users' information requires the `read:user` scope: From 15da749cfcc609fa9dfa55aeeb0d750e157cd741 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 09:13:50 -0600 Subject: [PATCH 10/16] Update docs/source/configuration/authorization.mdx Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 78f47ebb27..3a0afe4dfc 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -87,7 +87,7 @@ type Mutation { } ``` -You can define both directives—together or separately—at the field level to fine-tune your access controls. +You can define both directives—together or separately—at the field level to fine-tune your access controls. GraphOS [composes](#composition-and-federation) restrictions into the supergraph schema so that each subgraph's restrictions are respected. The router then enforces these directives on all incoming requests. From b1e60585cc77ed0b63d4bc2574447692b39ab3bf Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 09:34:15 -0600 Subject: [PATCH 11/16] Restructure prereqs --- docs/source/configuration/authorization.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 3a0afe4dfc..0ae055f3dd 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -93,11 +93,15 @@ The router then enforces these directives on all incoming requests. ## Prerequisites +Before using the authorization directives in your subgraph schemas, you must: +- Validate that your Apollo Router uses version `1.28.1` or later and is [connected to your GraphOS Enterprise organization](../enterprise-features/#enabling-enterprise-features) +- Include **[claims](#configure-request-claims)** in requests made to the router + ### Configure request claims -The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. +Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes—for example, `read:profiles`— assigned to that user. The authorization directives use a request's claims to evaluate which fields and types are authorized. -To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. +To provide the router with the claims it needs, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. - **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. - **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. @@ -105,10 +109,6 @@ To provide the router with the claims it needs to evaluate authorization directi ## Authorization directives -Before using the authorization directives in your subgraph schemas, ensure your router instances are ready to work with them: - - Make sure you're using version `1.28.1` or later of the Apollo Router. - - Make sure your router instances are [connected to your GraphOS Enterprise organization](/graphos/enterprise-features/#enabling-enterprise-features). - **Authorization directives are enabled in your router by default**. If you want to _disable_ them, include the following in your router's [YAML config file](./overview/): ```yaml title="router.yaml" From 37cc92beca6bfacd0718f730615f806a238fa4c1 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 09:53:36 -0600 Subject: [PATCH 12/16] Callout that gateway doesn't support auth directives --- docs/source/configuration/authorization.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 0ae055f3dd..b2c9844c2f 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,7 +1,7 @@ --- title: Authorization in the Apollo Router description: Strengthen service security with a centralized governance layer -minVersion: 1.28.1 +minVersion: 1.29.0 --- @@ -93,8 +93,10 @@ The router then enforces these directives on all incoming requests. ## Prerequisites +> ⚠️ Only the Apollo Router support authorization directives—[`@apollo/gateway`](/federation/v1/gateway/) does _not_. Checkout out the [migration guide](../migrating-from-gateway/) if you'd like to use them. + Before using the authorization directives in your subgraph schemas, you must: -- Validate that your Apollo Router uses version `1.28.1` or later and is [connected to your GraphOS Enterprise organization](../enterprise-features/#enabling-enterprise-features) +- Validate that your Apollo Router uses version `1.29.0` or later and is [connected to your GraphOS Enterprise organization](../enterprise-features/#enabling-enterprise-features) - Include **[claims](#configure-request-claims)** in requests made to the router ### Configure request claims From af024d07bc28c52ec15c6708be0983f62d54b686 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 11:48:37 -0600 Subject: [PATCH 13/16] Update docs/source/configuration/authorization.mdx Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index b2c9844c2f..1a21b54a1c 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -93,7 +93,7 @@ The router then enforces these directives on all incoming requests. ## Prerequisites -> ⚠️ Only the Apollo Router support authorization directives—[`@apollo/gateway`](/federation/v1/gateway/) does _not_. Checkout out the [migration guide](../migrating-from-gateway/) if you'd like to use them. +> ⚠️ Only the Apollo Router supports authorization directives—[`@apollo/gateway`](/federation/v1/gateway/) does _not_. Check out the [migration guide](../migrating-from-gateway/) if you'd like to use them. Before using the authorization directives in your subgraph schemas, you must: - Validate that your Apollo Router uses version `1.29.0` or later and is [connected to your GraphOS Enterprise organization](../enterprise-features/#enabling-enterprise-features) From 0dd6d33093ca485d11ae689de97d45ff2d6b1f02 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 1 Sep 2023 12:01:03 +0200 Subject: [PATCH 14/16] remove mentions of router service level rhai We'll add it back once that feature is merged --- docs/source/configuration/authorization.mdx | 33 --------------------- 1 file changed, 33 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 1a21b54a1c..1b6d1e47f8 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -140,39 +140,6 @@ claims = context["apollo_authentication::JWT::claims"] claims["scope"] = "scope1 scope2 scope3" ``` - - -If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). - -The example below extracts an array of scopes from the `"roles"` claim and reformats them as a space-separated string. - -```Rhai -fn router_service(service) { - let request_callback = |request| { - let claims = request.context["apollo_authentication::JWT::claims"]; - let roles = claims["roles"]; - - let scope = ""; - if roles.len() > 1 { - scope = roles[0]; - } - - if roles.len() > 2 { - for role in roles[1..] { - scope += ' '; - scope += role; - } - } - - claims["scope"] = scope; - request.context["apollo_authentication::JWT::claims"] = claims; - }; - service.map_request(request_callback); -} -``` - - - #### Usage To use the `@requiresScopes` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: From 8d72de790442962da5217d590d60cee1458cbd44 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 1 Sep 2023 12:01:55 +0200 Subject: [PATCH 15/16] Revert "remove mentions of router service level rhai" This reverts commit 0dd6d33093ca485d11ae689de97d45ff2d6b1f02. --- docs/source/configuration/authorization.mdx | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 1b6d1e47f8..1a21b54a1c 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -140,6 +140,39 @@ claims = context["apollo_authentication::JWT::claims"] claims["scope"] = "scope1 scope2 scope3" ``` + + +If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). + +The example below extracts an array of scopes from the `"roles"` claim and reformats them as a space-separated string. + +```Rhai +fn router_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + let roles = claims["roles"]; + + let scope = ""; + if roles.len() > 1 { + scope = roles[0]; + } + + if roles.len() > 2 { + for role in roles[1..] { + scope += ' '; + scope += role; + } + } + + claims["scope"] = scope; + request.context["apollo_authentication::JWT::claims"] = claims; + }; + service.map_request(request_callback); +} +``` + + + #### Usage To use the `@requiresScopes` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: From 5680bb8826ee40a4faa0db37b5d7ad2813398e11 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 5 Sep 2023 14:29:52 +0200 Subject: [PATCH 16/16] update changeset --- .../docs_geal_authorization_router_rhai.md | 5 ++++ .../feat_geal_authorization_directives.md | 24 ------------------- .../maint_geal_authorization_analytics.md | 8 ------- 3 files changed, 5 insertions(+), 32 deletions(-) create mode 100644 .changesets/docs_geal_authorization_router_rhai.md delete mode 100644 .changesets/feat_geal_authorization_directives.md delete mode 100644 .changesets/maint_geal_authorization_analytics.md diff --git a/.changesets/docs_geal_authorization_router_rhai.md b/.changesets/docs_geal_authorization_router_rhai.md new file mode 100644 index 0000000000..0e3596252b --- /dev/null +++ b/.changesets/docs_geal_authorization_router_rhai.md @@ -0,0 +1,5 @@ +### GraphOS authorization: add an example of scope manipulation with router service level rhai ([PR #3719](https://github.com/apollographql/router/pull/3719)) + +The router authorization directive `@requiresScopes` expects scopes to come from the `scope` claim in the OAuth2 access token format ( https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 ). Some tokens may have scopes stored in a different way, like an array of strings, or even in different claims. This documents a way to extract the scopes and prepare them in the right format for consumption by `@requiresScopes`, ushing Rhai. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3719 \ No newline at end of file diff --git a/.changesets/feat_geal_authorization_directives.md b/.changesets/feat_geal_authorization_directives.md deleted file mode 100644 index 453076cdc9..0000000000 --- a/.changesets/feat_geal_authorization_directives.md +++ /dev/null @@ -1,24 +0,0 @@ -### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397), [PR #3662](https://github.com/apollographql/router/pull/3662)) - -We introduce two new directives, `requiresScopes` and `@authenticated`, that define authorization policies for fields and types in the supergraph schema. - -They are defined as follows: - -```graphql -scalar federation__Scope -directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - -directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - -The implementation hooks into the request lifecycle at multiple steps: -- In query analysis, we extract the list of scopes necessary to authorize the query. -- In a supergraph plugin, we calculate the authorization status and put it in the request context: - - for `@requiresScopes`, this is the intersection of the query's required scopes and the scopes provided in the request token - - for `@authenticated`, it is `is_authenticated` or not -- In the query planning phase, we filter the query to remove unauthorized fields before proceeding with query planning. -- At the subgraph level, if query deduplication is active, the authorization status is used to group queries together. -- At the execution service level, the response is first formatted according to the filtered query, which removed any unauthorized information, then to the shape of the original query, which propagates nulls as needed. -- At the execution service level, errors are added to the response indicating which fields were removed because they were not authorized. - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397 https://github.com/apollographql/router/pull/3662 \ No newline at end of file diff --git a/.changesets/maint_geal_authorization_analytics.md b/.changesets/maint_geal_authorization_analytics.md deleted file mode 100644 index 50f235a639..0000000000 --- a/.changesets/maint_geal_authorization_analytics.md +++ /dev/null @@ -1,8 +0,0 @@ -### Add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) - -The new metric is a counter called `apollo.router.operations.authorization` and contains the following boolean attributes: -- `filtered`: the query has one or more filtered fields -- `requires_scopes`: the query uses fields or types tagged with the `@requiresScopes` directive -- `authenticated`: the query uses fields or types tagged with the `@authenticated` directive - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3660 \ No newline at end of file