From c7363469bb48aad9c9a52a60867542526a44be3f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 14:01:55 -0600 Subject: [PATCH 01/36] Add sub-header for authentication and authorization --- docs/source/config.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index 00f3fa5b5e..d773d63647 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -36,19 +36,21 @@ "Security": { "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", - "JWT Authentication": [ - "/configuration/authn-jwt", - [ - "enterprise" - ] - ], - "Authorization": [ + "Access control": { + "JWT Authentication": [ + "/configuration/authn-jwt", + [ + "enterprise" + ] + ], + "Authorization directives": [ "/configuration/authorization", [ "enterprise", "experimental" ] - ], + ] + }, "Operation limits": [ "/configuration/operation-limits", [ From d53751441596ea7e5145d51582ce322f6205e0ff Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 14:08:24 -0600 Subject: [PATCH 02/36] Restructure Authorization page --- docs/source/config.json | 2 +- docs/source/configuration/authorization.mdx | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index d773d63647..2c552fb556 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -43,7 +43,7 @@ "enterprise" ] ], - "Authorization directives": [ + "Authorization": [ "/configuration/authorization", [ "enterprise", diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8f5529efa8..a107b10d9d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,6 +6,9 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +## Authorization directives + The Apollo Router supports graph based authorization policies, through the `@authenticated` and `@requiresScopes` directives. They are used to authorize access to specific types or fields. They are defined as follows: @@ -18,7 +21,7 @@ directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INT To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). The JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. -## `@authenticated` +### `@authenticated` The `@authenticated` directive restricts access to fields and types if the request was not authenticated, by checking for the presence of the `apollo_authentication::JWT::claims` key in the request context. If those fields are restricted, the router will remove them entirely before planning the query, so the unauthenticated parts will still be executed and returned to the client, but the parts requiring authentication will never be requested from subgraphs, avoiding entire subgraph requests in some cases. @@ -100,7 +103,7 @@ So the `me` top level operation would not even be executed. When returning the r If all the fields were removed, then the router would generate a query planner error indicating that the query is unauthorized. -## `@requiresScopes` +### `@requiresScopes` The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. @@ -181,7 +184,6 @@ While if it presented the scopes `employee inventory`, it would filter the query And generate an error at path `/me`. - ## Composition and federation The authorization directives are defined by the subgraph author, and will be carried into the supergraph schema: @@ -280,7 +282,11 @@ query { } ``` -### Introspection +### Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. + +## Introspection Introspection is not affected by authorization, so all types fields will be accessible. The directives applied to them will not be visible though. If introspection might reveal too much information about internal types, then it should be deactivated like this: @@ -289,8 +295,4 @@ supergraph: introspection: false ``` -Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). - -### Query deduplication - -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. \ No newline at end of file +Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). \ No newline at end of file From 64e43e8649fdd596d3854c17136497d84263d2b6 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 16:06:56 -0600 Subject: [PATCH 03/36] Copy edit `@authenticated` section --- docs/source/configuration/authorization.mdx | 112 +++++++++++++------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index a107b10d9d..0a88535770 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,67 +6,103 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). +You may need to restrict sensitive information to authorized users or roles or enforce conditional access rules for particular fields. +The Apollo Router allows you to restrict access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: + +- The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. +- The `@requiresScopes` directive allows you granular access control through scopes that you define. + +You define and use the directives on a sub-graph level, and the router [composes](#composition) them to the supergraph level. + +## Prequisites + +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. +To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. ## Authorization directives -The Apollo Router supports graph based authorization policies, through the `@authenticated` and `@requiresScopes` directives. They are used to authorize access to specific types or fields. +### `@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. +If the request is authenticated, the router executes the query in its entirety. -They are defined as follows: +For unauthenticated requests, the router removes `@authenticated` fields before planning the query. +The router filters out fields that require authentication and only executes the parts of the query that don't require it. +If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. + +Define the `@authenticated` directive on any subgraph schema you want to use it on. ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). The JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. +#### Example `@authenticated` use case -### `@authenticated` - -The `@authenticated` directive restricts access to fields and types if the request was not authenticated, by checking for the presence of the `apollo_authentication::JWT::claims` key in the request context. If those fields are restricted, the router will remove them entirely before planning the query, so the unauthenticated parts will still be executed and returned to the client, but the parts requiring authentication will never be requested from subgraphs, avoiding entire subgraph requests in some cases. +Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. +You also want to have a query called `me` that returns the authenticated user. -As an example, assuming we have this schema: +Your schema may look something like this: ```graphql type Query { - me: User @authenticated - user(id: ID): User + me: User @authenticated + user(id: ID): User } type User { - id: ID - name: String - email: @authenticated + id: ID + name: String + email: String @authenticated } ``` -And this query: +Consider the following query: ```graphql -{ - me { - name - email - } - - user(id: 1234) { - name - email - } +query { + me { + name + email + } + user(id: "1234") { + name + email + } } ``` -If the request was authenticated, the entire query would be executed, as expected. But if it wasn't, the router would remove fields before execution, and create this filtered query: +An authenticated request would execute the entire query. +For an unauthenticated request, the router would remove the `@authenticated` fields before execution, and create a filtered query. -```graphql -{ - user(id: 1234) { - name - } + + +```graphql title="Query for an authenticated request" +query { + me { + name + email + } + user(id: "1234") { + name + email + } } ``` -So the `me` top level operation would not even be executed. When returning the response, the router will follow the initial request's shape and insert a null in the unauthorized fields, and apply the null propagation rules. It will generate a response of the following shape: +```graphql title="Filtered query for an unauthenticated request" +query { + user(id: "1234") { + name + } +} +``` + + +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user 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 { @@ -101,10 +137,14 @@ So the `me` top level operation would not even be executed. When returning the r } ``` -If all the fields were removed, then the router would generate a query planner error indicating that the query is unauthorized. +If every requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. ### `@requiresScopes` +```graphql +directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. Depending on which scope set is presented by the request, different parts of the query may be available. @@ -192,7 +232,7 @@ The authorization directives are defined by the subgraph author, and will be car ### Authorization and `@key` -When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [Contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). As an example, assuming we have these subgraphs definition: @@ -282,7 +322,7 @@ query { } ``` -### Query deduplication +## Query deduplication When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. @@ -295,4 +335,4 @@ supergraph: introspection: false ``` -Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). \ No newline at end of file +Fields can also be hidden using [contracts](/graphos/delivery/contracts/). \ No newline at end of file From a92ddae02b630361077fd5ab11aee61d97f26b7f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:01:17 -0600 Subject: [PATCH 04/36] Copy edit `@requiredScopes` section --- docs/source/configuration/authorization.mdx | 151 ++++++++++++-------- 1 file changed, 88 insertions(+), 63 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 0a88535770..8e1b225a59 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -31,7 +31,7 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -Define the `@authenticated` directive on any subgraph schema you want to use it on. +You can define the `@authenticated` directive on any subgraph schema like this: ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM @@ -97,13 +97,12 @@ query { } } ``` + For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user 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 { "data": { @@ -141,88 +140,128 @@ If every requested field requires authentication and a request is unauthenticate ### `@requiresScopes` +The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. +You can define the `@requiredScopes` directive on any subgraph schema like this: + ```graphql directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. +The directive should include a `scopes` argument that expects an array of required scopes. + +The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. +That object's `scope` key should contain a space separated list of scopes in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). -Depending on which scope set is presented by the request, different parts of the query may be available. +Depending on the scopes present on the request, the router filters out unauthorized fields and types. +If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. -Assuming we have this schema: +#### Example `@requiresScopes` use case + +Imagine your social media platform has an ecommerce component. +You need to be able to query for the following: + +- An authenticated user's profile (`me`) +- Other user's emails +- Products and the users who have ordered it + +Your schema may look something like this: ```graphql type Query { - me: User @requiresScopes(scopes: ["profile:read"]) - products: [Products] + me: User @requiresScopes(scopes: ["profile:read"]) + products: [Products] } type User { - id: ID - name: String - email: @requiresScopes(["user:read"]) + id: ID + name: String + email: @requiresScopes(["user:read"]) } type Product { - id: ID - name: String - amount: Int @requiresScopes(["employee", "inventory"]) - orders: [User] @requiresScopes(["employee"]) + id: ID + name: String + amount: Int @requiresScopes(["inventory:read"]) + orders: [User] @requiresScopes(["user:read", "inventory:read"]) } ``` -And this query: +The router executes the following query differently, depending on the request's attached scopes: ```graphql -{ - me { - name - email - } +query { + me { + name + email + } - products { - name - amount - orders { - name - } + products { + name + amount + orders { + name } + } } ``` -If the request presented the scopes `profile:read user:read`, then it would be interpreted as: +If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: +```graphql title="Request scopes: 'profile:read user:read'" +query { + me { + name + email + } -```graphql -{ - me { - name - email - } - - products { - name - } + products { + name + /* The following fields are filtered out: + amount # Requires "inventory:read" scope + orders # Requires "user:read" and "inventory:read" scopes + */ + } } ``` -And generate an error at path `/products/@/amount`. +The response would include errors at the `/products/@/amount` and `/products/@/amount` paths. -While if it presented the scopes `employee inventory`, it would filter the query as: +If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: -```graphql -{ - products { - name - amount - orders { - name - } +```graphql title="Request scopes: 'user:read inventory:read'" +query { + /* The following fields are filtered out + me { # Requires the "profile:read" scope + name + email + } + */ + products { + name + amount + orders { + name } + } } ``` -And generate an error at path `/me`. +The response would include an error at the `/me` path. + +## Introspection + +Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: + +```yaml +supergraph: + introspection: false +``` + +You can also hide fields using [contracts](/graphos/delivery/contracts/). + +## Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. ## Composition and federation @@ -322,17 +361,3 @@ query { } ``` -## Query deduplication - -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. - -## Introspection - -Introspection is not affected by authorization, so all types fields will be accessible. The directives applied to them will not be visible though. If introspection might reveal too much information about internal types, then it should be deactivated like this: - -```yaml -supergraph: - introspection: false -``` - -Fields can also be hidden using [contracts](/graphos/delivery/contracts/). \ No newline at end of file From 5e2d6251dbf614cdcdb9c33a2331f84686f068b8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:29:31 -0600 Subject: [PATCH 05/36] Copy edit "Authorization and `@key` types --- docs/source/configuration/authorization.mdx | 92 ++++++++++++++------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8e1b225a59..00de28c909 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -248,73 +248,91 @@ query { The response would include an error at the `/me` path. -## Introspection +## Composition and federation -Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: +Authorization directives are defined at the subgraph level and the router composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. -```yaml -supergraph: - introspection: false +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: + +```graphql title="Subgraph A" +type Query { + users: [User!]! @requiresScopes(scopes: ["profile:read"]) +} ``` -You can also hide fields using [contracts](/graphos/delivery/contracts/). +And another subgraph requires the `user:read` scope on `users` query: -## Query deduplication +```graphql title="Subgraph B" +type Query { + users: [User!]! @requiresScopes(scopes: ["user:read"]) +} +``` -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. +Then the supergraph schema would require both scopes for it. -## Composition and federation +```graphql title="Supergraph" +type Query { + users: [User!]! @requiresScopes(scopes: ["profile:read", "user:read"]) +} +``` -The authorization directives are defined by the subgraph author, and will be carried into the supergraph schema: -- `@authenticated`: if defined on a field or type by any of the subgraphs, it will be set in the supergraph too -- `@requiresScopes`: same as `@authenticated`, and if there are multiple applications, the supergraph schema will merge the sets of scopes required by each subgraph +### Authorization and `@key` types -### Authorization and `@key` +The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. -When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +If you use authorization directives on fields in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. -As an example, assuming we have these subgraphs definition: +Consider these example subgraph schemas: -```graphql -# subgraph A +```graphql title="Product subgraph" type Query { - t: T + product: Product } -type T @key(fields: "id") { +type Product @key(fields: "id") { id: ID! @authenticated + name: String! + price: Int @authenticated } ``` -```graphql -# subgraph B -type T @key(fields: "id") { +```graphql title="Inventory subgraph" +type Query { + product: Product +} + +type Product @key(fields: "id") { id: ID! @authenticated - value: Int + inStock: Boolean! } ``` -The following query can be done when not authenticated: +An unauthenticated request would successfully execute this query: ```graphql -{ - t { - value +query { + product { + name + inStock } } ``` -But this one would end up with a `null` in `id`, which would trigger nullability rules and nullify `t` too. +For this query: ```graphql -{ - t { +query { + product { id value } } ``` +An unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. + +This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). + ### Interfaces In the case one of the types implementing an interface requires authorization, then querying the interface will be allowed, but any parts that require access to that type will be filtered. @@ -361,3 +379,17 @@ query { } ``` +## Introspection + +Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: + +```yaml +supergraph: + introspection: false +``` + +You can also hide fields using [contracts](/graphos/delivery/contracts/). + +## Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. \ No newline at end of file From 2011701041dd50682056dd5e07855959a467f08e Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:48:35 -0600 Subject: [PATCH 06/36] Copy edit Interfaces --- docs/source/configuration/authorization.mdx | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 00de28c909..277d5227a6 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -279,7 +279,6 @@ type Query { ### Authorization and `@key` types 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 in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: @@ -318,70 +317,74 @@ query { } ``` -For this query: +But for this query: ```graphql query { product { id - value + name } } ``` -An unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. +an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. -This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +This behavior is similar to what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). -### Interfaces +### Authorization and interfaces -In the case one of the types implementing an interface requires authorization, then querying the interface will be allowed, but any parts that require access to that type will be filtered. +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. -As an example, with this schema, where the interface `I` does not require authentication, but the `User` type does: +For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: ```graphql type Query { - itf: I! + users: [User!]! } -interface I { - id: ID +interface User { + id: ID + name: String } -type User -implements I +type Admin +implements User @authenticated { id: ID name: String + role: String } ``` -If we send this query with an inline fragment and a type condition on `User`: +If an unauthenticated request were to make this query: ```graphql query { - itf { - id - ... on User { - name - } + users { + id + name + ... on Admin { + role } + } } ``` -The query would be filtered as follows: +The router woudl filter the query as follows: ```graphql query { - itf { + users { id + name } } ``` ## Introspection -Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: +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 to deactivate it: ```yaml supergraph: From 00f589641400e8c2b568a5dfe7144dbb909e5d7d Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:59:17 -0600 Subject: [PATCH 07/36] Copy edits --- docs/source/configuration/authorization.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 277d5227a6..b3f3f15e6f 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,17 +6,19 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authorized users or roles or enforce conditional access rules for particular fields. -The Apollo Router allows you to restrict access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +You may need to restrict sensitive information to authorized users or enforce conditional access rules for particular fields. +The Apollo Router allows you to control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. - The `@requiresScopes` directive allows you granular access control through scopes that you define. -You define and use the directives on a sub-graph level, and the router [composes](#composition) them to the supergraph level. +You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them to the supergraph. ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). + +The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. ## Authorization directives From 1ffdfd3c7d6a7bdbd18dbe38d0510f83f3370f36 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 18:06:34 -0600 Subject: [PATCH 08/36] Typo --- 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 b3f3f15e6f..9033ab92ce 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -373,7 +373,7 @@ query { } ``` -The router woudl filter the query as follows: +The router would filter the query as follows: ```graphql query { From 03cbade434edcb9d8a0acb34302d08990f3e8010 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 16:42:23 -0700 Subject: [PATCH 09/36] Add pre-req information --- docs/source/configuration/authorization.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 9033ab92ce..027277d478 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -12,14 +12,16 @@ The Apollo Router allows you to control access to specific fields and types thro - The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. - The `@requiresScopes` directive allows you granular access control through scopes that you define. -You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them to the supergraph. +You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). +Regardless of which you choose, you need to include **claims** in a request's context. +**Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. -The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. -To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. +If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. +To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. ## Authorization directives From cc0b141a85380ed37669e0ce712805208fae18a8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 16:59:58 -0700 Subject: [PATCH 10/36] Remove nested headers --- docs/source/config.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index 2c552fb556..f7bb37511d 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -36,8 +36,7 @@ "Security": { "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", - "Access control": { - "JWT Authentication": [ + "JWT Authentication": [ "/configuration/authn-jwt", [ "enterprise" @@ -49,8 +48,7 @@ "enterprise", "experimental" ] - ] - }, + ], "Operation limits": [ "/configuration/operation-limits", [ From a7c5949a85032c6a8a28897eeb67881703fe5390 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 17:00:59 -0700 Subject: [PATCH 11/36] Remove unnecessary space --- docs/source/config.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index f7bb37511d..00f3fa5b5e 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -37,11 +37,11 @@ "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", "JWT Authentication": [ - "/configuration/authn-jwt", - [ - "enterprise" - ] - ], + "/configuration/authn-jwt", + [ + "enterprise" + ] + ], "Authorization": [ "/configuration/authorization", [ From c6f355439dd5a4c0ca5989eb2379bec8f738ebbf Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 18 Jul 2023 12:45:53 -0700 Subject: [PATCH 12/36] Copy edit --- docs/source/configuration/authorization.mdx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 027277d478..e088b4d308 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,18 +6,17 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authorized users or enforce conditional access rules for particular fields. -The Apollo Router allows you to control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +You may need to restrict sensitive information to authenticated users or enforce conditional access rules for particular fields. +The Apollo Router lets you control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: -- The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. -- The `@requiresScopes` directive allows you granular access control through scopes that you define. +- The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. +- The `@requiresScopes` directive allows you granular access control through scopes you define. -You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and the router [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). -Regardless of which you choose, you need to include **claims** in a request's context. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that includes **claims** in a request's context. **Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. From 21f5a21ada3ef7f12c86571794299d4f993a664c Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 11:21:10 -0600 Subject: [PATCH 13/36] Apply suggestions from code review Co-authored-by: Lucas Leadbetter <5595530+lleadbet@users.noreply.github.com> --- docs/source/configuration/authorization.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 027277d478..fe17c07ecb 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -154,7 +154,7 @@ directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INT The directive should include a `scopes` argument that expects an array of required scopes. The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. -That object's `scope` key should contain a space separated list of scopes in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). +That object's `scope` key should contain a space separated list of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. @@ -280,10 +280,10 @@ type Query { } ``` -### Authorization and `@key` types +### 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 in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. +If you use authorization directives on fields in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: From e006b24cdeb767339f0e66d44cebf0ab8769c463 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 11:38:01 -0600 Subject: [PATCH 14/36] Clarify that `requiredScopes` can also eliminate entire subgraph requests --- docs/source/configuration/authorization.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index d6c55d26c8..45ab06153a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -157,6 +157,7 @@ That object's `scope` key should contain a space separated list of scopes in the Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. +If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Example `@requiresScopes` use case From 2dc22d2661aa2368f098419e9da6da93fdd603a0 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 14:17:32 -0600 Subject: [PATCH 15/36] Add authorization directives to list of router enterprise features --- docs/source/enterprise-features.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index aa2b2664a8..3b20406095 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) From 765c7f82cdc4154ac37462f15b4bb736f4d5ba44 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 28 Jul 2023 17:09:47 -0600 Subject: [PATCH 16/36] Update docs/source/configuration/authorization.mdx Co-authored-by: Simon Sapin --- 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 45ab06153a..5264fcb80a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -16,7 +16,7 @@ You define and use these directives on subgraph schemas, and the router [compose ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that includes **claims** in a request's context. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. From 1ff841fb2f68eff450e98d2e56a6f019b78d7a30 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 16:55:14 -0600 Subject: [PATCH 17/36] Apply suggestions from code review Co-authored-by: Chandrika Srinivasan --- 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 5264fcb80a..3be429d480 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -17,7 +17,7 @@ You define and use these directives on subgraph schemas, and the router [compose ## Prequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -**Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. +**Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. @@ -144,7 +144,7 @@ If every requested field requires authentication and a request is unauthenticate ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -You can define the `@requiredScopes` directive on any subgraph schema like this: +You can define the `@requiresScopes` directive on any subgraph schema like this: ```graphql directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM From ce51ab07e992d87947a18545b1becfe623ae7903 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 21 Jul 2023 12:50:20 -0600 Subject: [PATCH 18/36] Copy edits --- docs/source/configuration/authorization.mdx | 55 +++++++++++++-------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 3be429d480..63682fb57a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -80,7 +80,7 @@ For an unauthenticated request, the router would remove the `@authenticated` fie -```graphql title="Query for an authenticated request" +```graphql title="Query executed for an authenticated request" query { me { name @@ -93,7 +93,7 @@ query { } ``` -```graphql title="Filtered query for an unauthenticated request" +```graphql title="Query executed for an unauthenticated request" query { user(id: "1234") { name @@ -103,7 +103,7 @@ query { -For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user with `id: "1234"`. +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user 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 @@ -139,7 +139,7 @@ The response retains the initial request's shape but returns `null` for unauthor } ``` -If every requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. +If _every_ requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. ### `@requiresScopes` @@ -190,9 +190,13 @@ type Product { } ``` -The router executes the following query differently, depending on the request's attached scopes: +The router executes the following query differently, depending on the request's attached scopes. -```graphql +If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: + + + +```graphql title="Raw query to router" query { me { name @@ -209,9 +213,7 @@ query { } ``` -If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: - -```graphql title="Request scopes: 'profile:read user:read'" +```graphql title="Scopes: 'profile:read user:read'" query { me { name @@ -220,26 +222,25 @@ query { products { name - /* The following fields are filtered out: - amount # Requires "inventory:read" scope - orders # Requires "user:read" and "inventory:read" scopes - */ } } ``` -The response would include errors at the `/products/@/amount` and `/products/@/amount` paths. + + +The response would include errors at the `/products/@/amount` and `/products/@/orders` paths. If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: -```graphql title="Request scopes: 'user:read inventory:read'" + + +```graphql title="Raw query to router" query { - /* The following fields are filtered out - me { # Requires the "profile:read" scope + me { name email } - */ + products { name amount @@ -250,6 +251,20 @@ query { } ``` +```graphql title="Scopes: 'user:read inventory:read'" +query { + products { + name + amount + orders { + name + } + } +} +``` + + + The response would include an error at the `/me` path. ## Composition and federation @@ -321,7 +336,7 @@ query { } ``` -But for this query: +But 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 { @@ -332,8 +347,6 @@ query { } ``` -an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. - This behavior is similar to 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 From d74bd61148114ee231e249a8c8d1cca8eb137e6b Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 17:03:33 -0600 Subject: [PATCH 19/36] Copy edits and add to-do sections --- docs/source/configuration/authorization.mdx | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 63682fb57a..538832d193 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,13 +6,13 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authenticated users or enforce conditional access rules for particular fields. -The Apollo Router lets you control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. +The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. - The `@requiresScopes` directive allows you granular access control through scopes you define. -You define and use these directives on subgraph schemas, and the router [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites @@ -22,6 +22,14 @@ To use the router's authorization directives, you need to either configure [JWT If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. +### JWT authentication configuration + +To-do: Recipe for JWT auth with these directives + +### Coprocessors for authorization + +To-do: Coprocessors specifications + ## Authorization directives ### `@authenticated` @@ -269,7 +277,7 @@ The response would include an error at the `/me` path. ## Composition and federation -Authorization directives are defined at the subgraph level and the router composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: @@ -287,7 +295,7 @@ type Query { } ``` -Then the supergraph schema would require both scopes for it. +Then the supergraph schema would require _both_ scopes for it. ```graphql title="Supergraph" type Query { @@ -336,7 +344,9 @@ query { } ``` -But for the following query, an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. +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 { From 813e7a8171b323d1e24f50b0c646f006988427c7 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Sun, 6 Aug 2023 15:52:33 -0600 Subject: [PATCH 20/36] Copy edits --- docs/source/configuration/authorization.mdx | 66 +++++++++++++++------ 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 538832d193..d1db18caac 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,5 +1,7 @@ --- title: Authorization in the Apollo Router +description: Strengthen your supergraph's security with advanced access controls +minVersion: 1.27.0 --- > ⚠️ **This is an [Enterprise feature](../enterprise-features/) of the Apollo Router.** It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). @@ -9,10 +11,11 @@ title: Authorization in the Apollo Router Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: -- The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. -- The `@requiresScopes` directive allows you granular access control through scopes you define. +- The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. +- The `@requiresScopes` directive allows granular access control through scopes you define. You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. +The router then enforces these directives on all incoming requests. ## Prequisites @@ -42,7 +45,19 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -You can define the `@authenticated` directive on any subgraph schema like this: +#### Prequisites + +To use the `@authenticated` directive in a subgraph you can either: +- [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"]) +``` + +- or define the directive like this: ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM @@ -70,7 +85,7 @@ type User { Consider the following query: -```graphql +```graphql title="Sample query" query { me { name @@ -84,7 +99,7 @@ query { ``` An authenticated request would execute the entire query. -For an unauthenticated request, the router would remove the `@authenticated` fields before execution, and create a filtered query. +For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -114,7 +129,7 @@ query { For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user 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 +```json title="Unauthenticated request's response" { "data": { "me": null, @@ -152,12 +167,6 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -You can define the `@requiresScopes` directive on any subgraph schema like this: - -```graphql -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - The directive should include a `scopes` argument that expects an array of required scopes. The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. @@ -167,34 +176,53 @@ Depending on the scopes present on the request, the router filters out unauthori If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. +#### Prequisites + +To use the `@requiresScopes` directive in a subgraph you can either: +- [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"]) +``` + +- or define the directive like this: + +```graphql +directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. You need to be able to query for the following: - An authenticated user's profile (`me`) -- Other user's emails -- Products and the users who have ordered it +- Other users' emails +- Products and the users who have ordered them Your schema may look something like this: -```graphql +```graphql title="" type Query { me: User @requiresScopes(scopes: ["profile:read"]) - products: [Products] + products: [Product!] } type User { id: ID name: String email: @requiresScopes(["user:read"]) + products: [Product!] @requiresScopes(["user:read", "inventory:read"]) } type Product { id: ID name: String amount: Int @requiresScopes(["inventory:read"]) - orders: [User] @requiresScopes(["user:read", "inventory:read"]) + orders: [User!] @requiresScopes(["user:read", "inventory:read"]) } ``` @@ -363,7 +391,7 @@ This behavior is similar to what you can create with [contracts](/graphos/delive 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 `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: +For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: ```graphql type Query { @@ -409,6 +437,8 @@ query { } ``` +The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/users/@/role` path. + ## Introspection 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 to deactivate it: From 497f06bee11319b04434124956224fc2e2cd2a29 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Sun, 6 Aug 2023 16:32:39 -0600 Subject: [PATCH 21/36] Add to-dos --- docs/source/configuration/authorization.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index d1db18caac..f4e9a786dc 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -63,6 +63,8 @@ extend schema directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` +To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@authenticated` use case Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. @@ -194,6 +196,8 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` +To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. From 479961b04d29935ef92320bb0c1a3e3386476bdf Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:27:37 -0600 Subject: [PATCH 22/36] Update error message for completely filtered query --- 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 f4e9a786dc..8cb2a2f8b1 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -164,7 +164,7 @@ The response retains the initial request's shape but returns `null` for unauthor } ``` -If _every_ requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. +If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. ### `@requiresScopes` @@ -196,7 +196,7 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@requiresScopes` use case From 94b92820ff83e226a5fd796ad6f5dc87d5496922 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:28:21 -0600 Subject: [PATCH 23/36] Typo --- docs/source/configuration/authorization.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8cb2a2f8b1..c5e96ee110 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -45,7 +45,7 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -#### Prequisites +#### Prerequisites To use the `@authenticated` directive in a subgraph you can either: - [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -178,7 +178,7 @@ Depending on the scopes present on the request, the router filters out unauthori If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. -#### Prequisites +#### Prerequisites To use the `@requiresScopes` directive in a subgraph you can either: - [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -196,8 +196,6 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` - - #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. From a5ed4ab389a07d6ad3102665e7d341d881fe33bd Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:56:46 -0600 Subject: [PATCH 24/36] Typo --- 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 c5e96ee110..295e82c9fb 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -17,7 +17,7 @@ The Apollo Router lets you **control access to specific fields and types across You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. -## Prequisites +## Prerequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. From 5e2f3e13c18c7cce67afd79d0b8f0ea59a22f221 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 11:52:11 -0600 Subject: [PATCH 25/36] Rewrite intro --- docs/source/configuration/authorization.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 295e82c9fb..bcc887b8c1 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -8,7 +8,10 @@ minVersion: 1.27.0 > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. +APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks are essential to limit data to authorized parties. + +Enforcing authorization before processing requests is more efficient and secure because it allows for early request termination and creates an initial checkpoint that can be reinforced in other service layers. + The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. From d5ac25332503db25f67463471a86661df9a5acf4 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:01:12 -0600 Subject: [PATCH 26/36] Align code examples to demo --- docs/source/configuration/authorization.mdx | 270 +++++++++----------- 1 file changed, 126 insertions(+), 144 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index bcc887b8c1..66e6606875 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -8,11 +8,11 @@ minVersion: 1.27.0 > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks are essential to limit data to authorized parties. +APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is more efficient and secure because it allows for early request termination and creates an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: +The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. @@ -25,15 +25,16 @@ The router then enforces these directives on all incoming requests. To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. -If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. -To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. - ### JWT authentication configuration +If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. + To-do: Recipe for JWT auth with these directives ### Coprocessors for authorization +To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. + To-do: Coprocessors specifications ## Authorization directives @@ -41,17 +42,15 @@ To-do: Coprocessors specifications ### `@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. +It works by checking for the `apollo_authentication::JWT::claims` key in a request's context. If the request is authenticated, the router executes the query in its entirety. -For unauthenticated requests, the router removes `@authenticated` fields before planning the query. -The router filters out fields that require authentication and only executes the parts of the query that don't require it. +For unauthenticated requests, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. #### Prerequisites -To use the `@authenticated` directive in a subgraph you can either: -- [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +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 @@ -60,32 +59,34 @@ extend schema import: [..., "@authenticated"]) ``` -- or define the directive like this: - -```graphql -directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - -To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. - #### Example `@authenticated` use case -Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. -You also want to have a query called `me` that returns the authenticated user. +Suppose you are building a social media platform. Unauthenticated users can view all other parts of a public post—its title, author, etc. +However, you only want authenticated users to be able to see a post's number of views. +You also want to be able to query for an authenticated user's information. Your schema may look something like this: ```graphql type Query { me: User @authenticated - user(id: ID): User + post(id: ID!): Post } type User { - id: ID + id: ID! name: String - email: String @authenticated + posts: [Post!]! +} + +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated } + ``` Consider the following query: @@ -94,11 +95,10 @@ Consider the following query: query { me { name - email } - user(id: "1234") { - name - email + post(id: "1234") { + title + views } } ``` @@ -112,36 +112,34 @@ For an unauthenticated request, the router would remove the `@authenticated` fie query { me { name - email } - user(id: "1234") { - name - email + post(id: "1234") { + title + views } } ``` ```graphql title="Query executed for an unauthenticated request" query { - user(id: "1234") { - name + post(id: "1234") { + title } } ``` -For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user with `id: "1234"`. +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's response" { "data": { "me": null, - "user": { - "name": "Ada", - "email": null - } + "post": { + "title": "Securing supergraphs", + } }, "errors": [ { @@ -156,8 +154,8 @@ The response retains the initial request's shape but returns `null` for unauthor { "message": "Unauthorized field or type", "path": [ - "user", - "email" + "post", + "views" ], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" @@ -171,20 +169,28 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` -The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -The directive should include a `scopes` argument that expects an array of required scopes. +The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +The directive should include a `scopes` argument that defines an array of the required scopes. + +```graphql +@requiresScopes(scopes: ["scope1", "scope2", "scope3"]) +``` -The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. -That object's `scope` key should contain a space separated list of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). +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). + +``` +claims = context["apollo_authentication::JWT::claims"] +claims["scope"] = "scope1 scope2 scope3" +``` Depending on the scopes present on the request, the router filters out unauthorized fields and types. -If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. +If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Prerequisites -To use the `@requiresScopes` directive in a subgraph you can either: -- [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +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 @@ -193,138 +199,90 @@ extend schema import: [..., "@requiresScopes"]) ``` -- or define the directive like this: - -```graphql -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - #### Example `@requiresScopes` use case -Imagine your social media platform has an ecommerce component. -You need to be able to query for the following: - -- An authenticated user's profile (`me`) -- Other users' emails -- Products and the users who have ordered them +Imagine your social media platform lets users view other users' information only if they have the required permissions. Your schema may look something like this: ```graphql title="" type Query { - me: User @requiresScopes(scopes: ["profile:read"]) - products: [Product!] + me: User @authenticated + user(id: ID!): User @requiresScopes(scopes: ["read:others"]) + users: [User!]! @requiresScopes(scopes: ["read:others"]) } type User { - id: ID + id: ID! name: String - email: @requiresScopes(["user:read"]) - products: [Product!] @requiresScopes(["user:read", "inventory:read"]) + email: String @requiresScopes(scopes: ["read:email"]) + profileImage: String + posts: [Post!]! } -type Product { - id: ID - name: String - amount: Int @requiresScopes(["inventory:read"]) - orders: [User!] @requiresScopes(["user:read", "inventory:read"]) +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated } ``` The router executes the following query differently, depending on the request's attached scopes. -If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: +If the request includes only the `read:others` scope, then the router would execute the following filtered query: ```graphql title="Raw query to router" query { - me { + users { name + profileImage email } - - products { - name - amount - orders { - name - } - } } ``` -```graphql title="Scopes: 'profile:read user:read'" +```graphql title="Scopes: 'read:others'" query { - me { - name - email - } - - products { + users { name + profileImage } } ``` -The response would include errors at the `/products/@/amount` and `/products/@/orders` paths. +The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: +If the request includes the `read:others read:emails` scope set, then the router could successfully execute the entire query. - + -```graphql title="Raw query to router" -query { - me { - name - email - } +Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! - products { - name - amount - orders { - name - } - } -} -``` - -```graphql title="Scopes: 'user:read inventory:read'" -query { - products { - name - amount - orders { - name - } - } -} -``` - - - -The response would include an error at the `/me` path. + ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. -If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: ```graphql title="Subgraph A" type Query { - users: [User!]! @requiresScopes(scopes: ["profile:read"]) + users: [User!]! @requiresScopes(scopes: ["read:others"]) } ``` -And another subgraph requires the `user:read` scope on `users` query: +And another subgraph requires the read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { - users: [User!]! @requiresScopes(scopes: ["user:read"]) + users: [User!]! @requiresScopes(scopes: ["read:profiles"]) } ``` @@ -332,14 +290,14 @@ Then the supergraph schema would require _both_ scopes for it. ```graphql title="Supergraph" type Query { - users: [User!]! @requiresScopes(scopes: ["profile:read", "user:read"]) + users: [User!]! @requiresScopes(scopes: ["read:others", "read:profiles"]) } ``` ### 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 in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. +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: @@ -396,24 +354,40 @@ This behavior is similar to what you can create with [contracts](/graphos/delive 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 `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type which implements `Post` does: ```graphql type Query { - users: [User!]! + post(id: ID!): Post } -interface User { - id: ID +type User { + id: ID! name: String + posts: [Post!]! } -type Admin -implements User -@authenticated { - id: ID - name: String - role: String +interface Post { + id: ID! + author: User! + title: String! + content: String! +} + +type PublicBlog implements 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!]! } ``` @@ -421,11 +395,12 @@ If an unauthenticated request were to make this query: ```graphql query { - users { + posts { id - name - ... on Admin { - role + author + title + ... on PrivateBlog { + allowedViewers } } } @@ -435,14 +410,21 @@ The router would filter the query as follows: ```graphql query { - users { + posts { id - name + author + title } } ``` -The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/users/@/role` path. +The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. + + + +Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! + + ## Introspection From fa7ff60df0ad47b836da9831452a61bc055e8748 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:17:31 -0600 Subject: [PATCH 27/36] Copy edits --- docs/source/configuration/authorization.mdx | 35 ++++++++++----------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 66e6606875..43a1b20f03 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -4,9 +4,13 @@ description: Strengthen your supergraph's security with advanced access controls minVersion: 1.27.0 --- -> ⚠️ **This is an [Enterprise feature](../enterprise-features/) of the Apollo Router.** It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). -> -> If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). +
+ +**This feature is available only with a [GraphOS Enterprise plan](/graphos/enterprise/).** It is currently in [preview](/resources/product-launch-stages#preview). + +If your organization _doesn't_ currently have an Enterprise plan, you can test this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +
APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. @@ -23,13 +27,13 @@ The router then enforces these directives on all incoming requests. ## Prerequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -**Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any 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 associated user and/or any scopes assigned to that user. ### JWT authentication configuration If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. -To-do: Recipe for JWT auth with these directives +To-do: More information on how to use JWT auth with these directives ### Coprocessors for authorization @@ -186,7 +190,8 @@ claims["scope"] = "scope1 scope2 scope3" Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. -If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. + +Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Prerequisites @@ -202,7 +207,6 @@ extend schema #### Example `@requiresScopes` use case Imagine your social media platform lets users view other users' information only if they have the required permissions. - Your schema may look something like this: ```graphql title="" @@ -258,11 +262,11 @@ query { The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `read:others read:emails` scope set, then the router could successfully execute the entire query. +If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. -Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! +Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! @@ -278,7 +282,7 @@ type Query { } ``` -And another subgraph requires the read:profiles` scope on `users` query: +And another subgraph requires the `read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { @@ -358,7 +362,7 @@ For example, consider this schema where the `Post` interface doesn't require aut ```graphql type Query { - post(id: ID!): Post + posts: [Post!]! } type User { @@ -374,13 +378,6 @@ interface Post { content: String! } -type PublicBlog implements Post { - id: ID! - author: User! - title: String! - content: String! -} - type PrivateBlog implements Post @authenticated { id: ID! author: User! @@ -422,7 +419,7 @@ The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/post -Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! +Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! From 8a3fa5892cf22cd4c997c32e6f1948c3f257f762 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:53:08 -0600 Subject: [PATCH 28/36] Remove links to demo --- docs/source/configuration/authorization.mdx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 43a1b20f03..316db5ad33 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -264,12 +264,6 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. - - -Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! - - - ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. @@ -417,12 +411,6 @@ query { The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. - - -Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! - - - ## Introspection 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 to deactivate it: From 2ab7433bda41b62f6d3cd6cbe0aae547b4f58ee9 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 9 Aug 2023 16:11:42 +0200 Subject: [PATCH 29/36] move the claim augmentation example --- docs/source/configuration/authn-jwt.mdx | 44 ------------- docs/source/configuration/authorization.mdx | 68 ++++++++++++++++++++- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/docs/source/configuration/authn-jwt.mdx b/docs/source/configuration/authn-jwt.mdx index a01c5c40dc..ed072b93cd 100644 --- a/docs/source/configuration/authn-jwt.mdx +++ b/docs/source/configuration/authn-jwt.mdx @@ -263,50 +263,6 @@ fn subgraph_service(service, subgraph) { -### Claim augmentation via coprocessors - -Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). - - - -The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. - -If the router is configured with: - -```yaml title="router.yaml" -authentication: - jwt: - jwks: - - url: "file:///etc/router/jwks.json" - -coprocessor: - url: http://127.0.0.1:8081 - router: - request: - context: true -``` - -The coprocessor will then receive a request with this format: - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" - } - } - }, - "method": "POST" -} -``` - - ## Creating your own JWKS (advanced) > ⚠️ **Most third-party IdP services create and host a JWKS for you.** If you use a third-party IdP, consult its documentation to obtain the [JWKS URL](#jwks) to pass to your router. diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 316db5ad33..c57df67778 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -35,11 +35,73 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router automatica To-do: More information on how to use JWT auth with these directives -### Coprocessors for authorization -To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. +### Claim augmentation via coprocessors -To-do: Coprocessors specifications +Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). + + + +The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. + +If the router is configured with: + +```yaml title="router.yaml" +authentication: + jwt: + jwks: + - url: "file:///etc/router/jwks.json" + +coprocessor: + url: http://127.0.0.1:8081 + router: + request: + context: true +``` + +The coprocessor can then receive a request with this format: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + } + } + }, + "method": "POST" +} +``` + +The coprocessor would then look up the user with identifier specified in the `sub` claim, and return a response with more claims: + + +```json +{ + // Control properties + "version": 1, + "stage": "RouterResponse", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a", + "scope": "profile:read profile:write" + } + } + } +} +``` + + ## Authorization directives From 2791253d26a322215f8eb058d1a148b1c50e2a6f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 10 Aug 2023 10:00:21 +0200 Subject: [PATCH 30/36] Update docs/source/configuration/authorization.mdx --- docs/source/configuration/authorization.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index c57df67778..5db7c3cdc3 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -48,9 +48,10 @@ If the router is configured with: ```yaml title="router.yaml" authentication: - jwt: - jwks: - - url: "file:///etc/router/jwks.json" + router: + jwt: + jwks: + - url: "file:///etc/router/jwks.json" coprocessor: url: http://127.0.0.1:8081 From 141b84cd3ef73c2af0d0b5051f400c21e1774f83 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 12:05:47 +0200 Subject: [PATCH 31/36] add documentation for --- docs/source/configuration/authorization.mdx | 163 +++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 5db7c3cdc3..6d56287be2 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -16,10 +16,11 @@ APIs provide access to business-critical data. Unrestricted access can result in Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. +- The `@policy` directive offloads the authorization policy execution to Rhai or a coprocessor, and integrates the result with Router authorization You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. @@ -87,7 +88,7 @@ The coprocessor would then look up the user with identifier specified in the `su { // Control properties "version": 1, - "stage": "RouterResponse", + "stage": "RouterRequest", "control": "continue", "id": "d0a8245df0efe8aa38a80dba1147fb2e", "context": { @@ -327,6 +328,164 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +### `@policy` + +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. +The directive should include a `policies` argument that defines an array of the required policies. + +```graphql +@policy(policies: ["claims[`roles`].contains(`support`)"]) +``` + +The Apollo Router extract from the schema the list of policies relevant to the query, and stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This is done at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router will filter types and fields for which the policies failed or were not executed. +If the `policies` array contains multiple elements, one of them has to be successful to keep the field. + +Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires policies that fail, this can eliminate entire subgraph requests. + +#### Prerequisites + +To use the `@policy` 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: [..., "@policy"]) +``` + +It requires a Supergraph plugin to evaluate the authorization policies. This can be done with Rhaiscript, a coprocessor or a native plugin. + +##### Usage with Rhaiscript + + + +The `policies` argument contains a list of strings, with no format constraints, so we can use them to store Rhai code. As an example, with the following schema, we define policies as boolean expressions that will be evaluated in Rhai: + +```graphql +type Query { + me: User @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`kind`] == `user`"]) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`roles`].contains(`support`)"]) +} +``` + +We can then load the following Rhai file: + +``` +fn supergraph_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + + let policies = request.context["apollo_authorization::policies::required"]; + + for key in policies.keys() { + let result = eval(key); + policies[key] = result; + } + + request.context["apollo_authorization::policies::required"] = policies; + }; + service.map_request(request_callback); +} +``` + +For each policy, it will evaluate it and store the result in the `apollo_authorization::policies::required` map. + + + +##### Usage with a coprocessor + +A [coprocessor](../customizations/coprocessor) called at the Supergraph request stage can receive the list of policies and execute them. It is useful to bridge the Router authorization with an existing authorization stack, or link policy execution with lookups in a database. + + + +If the router is configured with: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 + supergraph: + request: + context: true +``` + +And a schema like this: + +```graphql +type Query { + me: User @join__field(graph: ACCOUNTS) @policy(policies: ["read_profile"]) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + credit_card: String @join__field(graph: ACCOUNTS) @policy(policies: ["read_credit_card"]) +} +``` + +The coprocessor can then receive a request with this format: + +```json +{ + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo_authorization::policies::required": { + "read_profile": null, + "read_address": null + } + } + }, + "method": "POST" +} +``` + +A user can read their own profile, so `read_profile` will succeed. But only the billing system should be able to see the credit card, so `read_credit_card` will fail. The corpocessor will then return: + + +```json +{ + // Control properties + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo_authorization::policies::required": { + "read_profile": true, + "read_address": false + } + } + } +} +``` + + + +#### Example `@policy` use case + +TODO + ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. From 9a5f0de387eb6b81c84065715af3314bd9e120b5 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 17:29:06 +0200 Subject: [PATCH 32/36] WiP: Rhai script to edit the claims this cannot work yet because router service level scripts don't work yet --- docs/source/configuration/authorization.mdx | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 6d56287be2..af921214db 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -328,6 +328,35 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +#### Scopes claims in other formats + +If the token holds scopes in another format (ex: an array of strings) or in another claim, it is possible to edit the claims with a Rhai script while the request is going through the router: + +``` +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); +} +``` + ### `@policy` The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. From 77f536816ad490d148b16a357d281de443283ab3 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 13:21:26 -0600 Subject: [PATCH 33/36] Use content components --- docs/source/configuration/authorization.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index af921214db..7ebfb24f0e 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -3,18 +3,13 @@ title: Authorization in the Apollo Router description: Strengthen your supergraph's security with advanced access controls minVersion: 1.27.0 --- + -
- -**This feature is available only with a [GraphOS Enterprise plan](/graphos/enterprise/).** It is currently in [preview](/resources/product-launch-stages#preview). - -If your organization _doesn't_ currently have an Enterprise plan, you can test this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). - -
+ APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization before processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: From 3459afb479161c123b5d1fe29f5cb4cc74936c59 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 14:29:36 -0600 Subject: [PATCH 34/36] Copy edits --- docs/source/configuration/authorization.mdx | 174 ++++++++++---------- 1 file changed, 85 insertions(+), 89 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 7ebfb24f0e..0656c84d37 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -15,7 +15,7 @@ The Apollo Router provides fine-grained access control at your graph's edge. Usi - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. -- The `@policy` directive offloads the authorization policy execution to Rhai or a coprocessor, and integrates the result with Router authorization +- The `@policy` directive offloads authorization policy execution to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. @@ -31,13 +31,10 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router automatica To-do: More information on how to use JWT auth with these directives - ### Claim augmentation via coprocessors Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). - - The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. If the router is configured with: @@ -98,8 +95,6 @@ The coprocessor would then look up the user with identifier specified in the `su } ``` - - ## Authorization directives ### `@authenticated` @@ -108,10 +103,10 @@ The `@authenticated` directive marks specific fields and types as requiring auth It works by checking for the `apollo_authentication::JWT::claims` key in a request's context. If the request is authenticated, the router executes the query in its entirety. -For unauthenticated requests, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. -If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. +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. +If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests, thereby increasing router efficiency. -#### Prerequisites +#### 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: @@ -124,9 +119,9 @@ extend schema #### Example `@authenticated` use case -Suppose you are building a social media platform. Unauthenticated users can view all other parts of a public post—its title, author, etc. -However, you only want authenticated users to be able to see a post's number of views. -You also want to be able to query for an authenticated user's information. +Suppose you're building a social media platform. Unauthenticated users can view a public post's title, author, and content. +However, you only want authenticated users to be able to see the number of views a post has received. +You also need to be able to query for an authenticated user's information. Your schema may look something like this: @@ -138,7 +133,7 @@ type Query { type User { id: ID! - name: String + username: String posts: [Post!]! } @@ -157,7 +152,7 @@ Consider the following query: ```graphql title="Sample query" query { me { - name + username } post(id: "1234") { title @@ -166,7 +161,7 @@ query { } ``` -An authenticated request would execute the entire query. +The router would executed the entire request if it's authenticated. For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -174,7 +169,7 @@ For an unauthenticated request, the router would remove the `@authenticated` fie ```graphql title="Query executed for an authenticated request" query { me { - name + username } post(id: "1234") { title @@ -233,26 +228,59 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +Depending on the scopes present on the request, the router filters out unauthorized fields and types. The directive should include a `scopes` argument that defines an array of the required scopes. ```graphql @requiresScopes(scopes: ["scope1", "scope2", "scope3"]) ``` +> If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. + 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" ``` -Depending on the scopes present on the request, the router filters out unauthorized fields and types. -If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. + + +If the `apollo_authentication::JWT::claims` token holds scopes in another format, for example, an array of strings, or in another claim, you can edit the claims with a [custom 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); +} +``` + + Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. -#### Prerequisites +#### 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: @@ -265,7 +293,7 @@ extend schema #### Example `@requiresScopes` use case -Imagine your social media platform lets users view other users' information only if they have the required permissions. +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 title="" @@ -273,11 +301,12 @@ type Query { me: User @authenticated user(id: ID!): User @requiresScopes(scopes: ["read:others"]) users: [User!]! @requiresScopes(scopes: ["read:others"]) + post(id: ID!): Post } type User { id: ID! - name: String + username: String email: String @requiresScopes(scopes: ["read:email"]) profileImage: String posts: [Post!]! @@ -301,7 +330,7 @@ If the request includes only the `read:others` scope, then the router would exec ```graphql title="Raw query to router" query { users { - name + username profileImage email } @@ -311,7 +340,7 @@ query { ```graphql title="Scopes: 'read:others'" query { users { - name + username profileImage } } @@ -319,54 +348,25 @@ query { -The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. +The response would include an errors at the `/users/@/email` path since that field requires the `read:emails` scope. If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. -#### Scopes claims in other formats - -If the token holds scopes in another format (ex: an array of strings) or in another claim, it is possible to edit the claims with a Rhai script while the request is going through the router: - -``` -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); -} -``` - ### `@policy` -The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. -The directive should include a `policies` argument that defines an array of the required policies. +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a [Rhai script](../customizations/rhai/) or[coprocessor](../customizations/coprocessor). +The directive should include a `policies` argument that defines an array of the required policies. The example below includes one policy that requires all roles from the claims object to include the string `"support"`. ```graphql @policy(policies: ["claims[`roles`].contains(`support`)"]) ``` -The Apollo Router extract from the schema the list of policies relevant to the query, and stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This is done at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router will filter types and fields for which the policies failed or were not executed. -If the `policies` array contains multiple elements, one of them has to be successful to keep the field. +The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This happens at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. +If the `policies` array contains multiple elements, only one of them has to be successful for the policy to be `true`. -Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires policies that fail, this can eliminate entire subgraph requests. +Like the efficiencies gained via the authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. -#### Prerequisites +#### Usage To use the `@policy` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -377,13 +377,13 @@ extend schema import: [..., "@policy"]) ``` -It requires a Supergraph plugin to evaluate the authorization policies. This can be done with Rhaiscript, a coprocessor or a native plugin. +Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a a [Rhai script](../customizations/rhai/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. -##### Usage with Rhaiscript +##### Usage with a Rhai script -The `policies` argument contains a list of strings, with no format constraints, so we can use them to store Rhai code. As an example, with the following schema, we define policies as boolean expressions that will be evaluated in Rhai: +The `policies` argument contains a list of strings, with no formatting constraints, so you can use them to store Rhai code. As an example,the following schema defines policies as boolean expressions that will be evaluated in Rhai: ```graphql type Query { @@ -394,12 +394,12 @@ type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) username: String @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`roles`].contains(`support`)"]) } ``` -We can then load the following Rhai file: +You can then use the following Rhai script to evaluate the policies: ``` fn supergraph_service(service) { @@ -419,27 +419,17 @@ fn supergraph_service(service) { } ``` -For each policy, it will evaluate it and store the result in the `apollo_authorization::policies::required` map. +The script uses the [`eval` function](https://rhai.rs/book/ref/eval.html) to evaluate each policy and store the result in the `apollo_authorization::policies::required` map. ##### Usage with a coprocessor -A [coprocessor](../customizations/coprocessor) called at the Supergraph request stage can receive the list of policies and execute them. It is useful to bridge the Router authorization with an existing authorization stack, or link policy execution with lookups in a database. +You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive the list of policies and execute them. This is useful to bridge the router authorization with an existing authorization stack, or link policy execution with lookups in a database. -If the router is configured with: - -```yaml title="router.yaml" -coprocessor: - url: http://127.0.0.1:8081 - supergraph: - request: - context: true -``` - -And a schema like this: +Suppose you only want a user with a `read_profile` policy to have access to their own information. An additional policy `read_credit_card` is required to access credit card information. Your schema may look something like this: ```graphql type Query { @@ -450,12 +440,22 @@ type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) credit_card: String @join__field(graph: ACCOUNTS) @policy(policies: ["read_credit_card"]) } ``` -The coprocessor can then receive a request with this format: +If you configure your router like this: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 + supergraph: + request: + context: true +``` + +then a coprocessor can then receive a request with this format: ```json { @@ -506,13 +506,9 @@ A user can read their own profile, so `read_profile` will succeed. But only the -#### Example `@policy` use case - -TODO - ## Composition and federation -Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: @@ -552,7 +548,7 @@ type Query { type Product @key(fields: "id") { id: ID! @authenticated - name: String! + username: String! price: Int @authenticated } ``` @@ -573,7 +569,7 @@ An unauthenticated request would successfully execute this query: ```graphql query { product { - name + username inStock } } @@ -587,7 +583,7 @@ For the following query, an unauthenticated request would resolve `null` for `id query { product { id - name + username } } ``` @@ -607,7 +603,7 @@ type Query { type User { id: ID! - name: String + username: String posts: [Post!]! } From cb2b46ff3665c42e2a90867bda15dbefd102ebfd Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 16:11:06 -0600 Subject: [PATCH 35/36] Copy edit --- docs/source/configuration/authorization.mdx | 93 ++++++++++++--------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 0656c84d37..6ff5e7f908 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -3,41 +3,45 @@ title: Authorization in the Apollo Router description: Strengthen your supergraph's security with advanced access controls 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 APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization _before_ processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated`, `@requiresScopes`, and `@policy` directives, you can define access to specific fields and types across your supergraph: -- The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. -- The `@requiresScopes` directive allows granular access control through scopes you define. -- The `@policy` directive offloads authorization policy execution to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. +- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +- The [`@policy`](#policy) directive offloads authorization validation to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. It's useful when your authorization policies go beyond simple authentication and scopes. -You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. ## Prerequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. - -### JWT authentication configuration +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 associated user and any authorization scopes assigned to that user. -If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. +To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#claim-augmentation-via-coprocessors) (explained below), you may require both. -To-do: More information on how to use JWT auth with these directives +### 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. These claims are then accessible to the authorization directives to evaluate which fields and types are authorized. + ### Claim augmentation via coprocessors -Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). +Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](/customizations/coprocessor). -The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. +A [`RouterService` coprocessor](/customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: +- receive the list of claims extracted from the token +- use information like the `sub` (subject) claim to look up the user in an external database or service +- insert additional data in the claims list +- return the claims list to the router -If the router is configured with: +For example, if you use this [router configuration](/configuration/overview#yaml-config-file): ```yaml title="router.yaml" authentication: @@ -73,7 +77,7 @@ The coprocessor can then receive a request with this format: } ``` -The coprocessor would then look up the user with identifier specified in the `sub` claim, and return a response with more claims: +The coprocessor can then look up the user with the identifier specified in the `sub` claim and return a response with more claims: ```json @@ -108,7 +112,7 @@ If every field in a particular subgraph's query is marked as requiring authentic #### 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: +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 @@ -120,7 +124,7 @@ extend schema #### Example `@authenticated` use case Suppose you're building a social media platform. Unauthenticated users can view a public post's title, author, and content. -However, you only want authenticated users to be able to see the number of views a post has received. +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. Your schema may look something like this: @@ -161,7 +165,7 @@ query { } ``` -The router would executed the entire request if it's authenticated. +The router would execute the entire query in an authenticated request. For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -228,17 +232,18 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. -Depending on the scopes present on the request, the router filters out unauthorized fields and types. -The directive should include a `scopes` argument that defines an array of the 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. + > If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. 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). +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"] @@ -247,9 +252,9 @@ claims["scope"] = "scope1 scope2 scope3" -If the `apollo_authentication::JWT::claims` token holds scopes in another format, for example, an array of strings, or in another claim, you can edit the claims with a [custom Rhai script](../customizations/rhai). +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. +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) { @@ -282,7 +287,7 @@ Like the efficiencies gained via the `@authenticated` directive, if every field #### 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: +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 @@ -321,9 +326,8 @@ type Post { } ``` -The router executes the following query differently, depending on the request's attached scopes. - -If the request includes only the `read:others` scope, then the router would execute the following filtered query: +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 will execute the following filtered query: @@ -348,27 +352,32 @@ query { -The response would include an errors at the `/users/@/email` path since that field requires the `read:emails` scope. +The response would include an error at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +If the request includes the `read:others read:emails` scope set, the router can execute the entire query successfully. ### `@policy` -The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a [Rhai script](../customizations/rhai/) or[coprocessor](../customizations/coprocessor). -The directive should include a `policies` argument that defines an array of the required policies. The example below includes one policy that requires all roles from the claims object to include the string `"support"`. +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a [Rhai script](../customizations/rhai/) or [coprocessor](../customizations/coprocessor). This enables custom authorization validation beyond authentication and scopes. +The directive should include a `policies` argument that defines an array of the required policies. ```graphql @policy(policies: ["claims[`roles`].contains(`support`)"]) ``` -The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This happens at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. -If the `policies` array contains multiple elements, only one of them has to be successful for the policy to be `true`. +The preceding example includes one policy that requires all roles from the claims object to include the string `"support"`. + +The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context in `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This process happens at the [`RouterService` level](../customizations/overview#the-request-lifecycle). -Like the efficiencies gained via the authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. +You need to provide a Rhai script or a coprocessor at the `SupergraphService` level that evaluates the map, setting the value to `true` if the policy is validated and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. + +> If the `policies` array contains multiple elements, _only one must be successful for the policy to be `true`_. + +Like the efficiencies gained via the other authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. #### Usage -To use the `@policy` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +To use the `@policy` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: ```graphql extend schema @@ -377,13 +386,15 @@ extend schema import: [..., "@policy"]) ``` -Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a a [Rhai script](../customizations/rhai/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. +Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a [Rhai script](../customizations/rhai/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. ##### Usage with a Rhai script +The `policies` argument contains a list of strings with no formatting constraints. That means you can use them to store Rhai code. + -The `policies` argument contains a list of strings, with no formatting constraints, so you can use them to store Rhai code. As an example,the following schema defines policies as boolean expressions that will be evaluated in Rhai: +As an example, the following schema defines policies as boolean expressions that can be evaluated in Rhai: ```graphql type Query { @@ -425,7 +436,7 @@ The script uses the [`eval` function](https://rhai.rs/book/ref/eval.html) to eva ##### Usage with a coprocessor -You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive the list of policies and execute them. This is useful to bridge the router authorization with an existing authorization stack, or link policy execution with lookups in a database. +You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive and execute the list of policies. This is useful to bridge router authorization with an existing authorization stack or link policy execution with lookups in a database. @@ -508,7 +519,7 @@ A user can read their own profile, so `read_profile` will succeed. But only the ## Composition and federation -Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level, and GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: @@ -594,7 +605,7 @@ This behavior is similar to what you can create with [contracts](/graphos/delive 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: +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post` does: ```graphql type Query { From 84d1054c1e2693414bec015ae54ea60acd964bd0 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 16:29:52 -0600 Subject: [PATCH 36/36] Update intro --- 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 6ff5e7f908..b8b874c93d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -10,7 +10,9 @@ 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 APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization _before_ processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization in the Apollo Router is valuable for a few reasons: +- Validating authorization _before_ processing requests allows for early request termination. +- It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated`, `@requiresScopes`, and `@policy` directives, you can define access to specific fields and types across your supergraph: @@ -599,7 +601,7 @@ query { } ``` -This behavior is similar to what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +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