-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Directives
Since Federation v2.5, three directives are introduced @authenticated
, @requiresScopes
and @policy
-
Granular regular protection with
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
;
When you add this directive in your subgraph, it is directly added in the supergraph during composition. So it marks the component resolvable only if defined auth conditions met. -
RBAC with
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
This directive becomes resolvable by the user only when the set of scopes are available in the auth payload.
AND / OR
;
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@requiresScopes"])
type Query {
# This field requires the user to have `read:user` OR `read:admin` scopes
me: User! @requiresScopes(scopes: [["read:user"], ["read:admin"]])
# This field requires the user to have `read:user` AND `read:admin` scopes
protectedField: String @requiresScopes(scopes: [["read:admin", "read:user"]])
publicField: String
}
- Dynamic RBAC with
directive @policy(policies: [[federation__Policy!]!]!) on | FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
Same with@requiresScopes
but it requires an external call/logic to fetch policies which are treated in a similar way withscopes
above.
So everytime we need to compare policies, the gateway needs to call something like below;
fetchPolicies: async user => {
const res = await fetch('https://policy-service.com', {
headers: {
Authorization: `Bearer ${user.token}`
}
})
// Expected to return an array of strings
return res.json()
}
Example Scenarios
The scenarios below show how the gateway behaves with the directives
Scenario 1: Accessing @authenticated
field when unauthorized
We have the supergraph schema below;
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5"
import: ["@authenticated"]
)
type Query {
me: User @authenticated
post(id: ID!): Post
}
type User {
id: ID!
username: String
email: String
posts: [Post!]!
}
type Post {
id: ID!
author: User!
title: String!
content: String!
views: Int @authenticated
}
And we are not authorized, and trying to access Post.views
and User.me
with the following query;
query {
me {
username
}
post(id: "1") {
title
views
}
}
In that case, except views
and me
, all other fields will be resolved, and the gateway will throw 2 errors with the paths and the codes.
The path in the response should point to the nullified
field, locations
should point to the place in the original operation's selection.
{
data: {
me: null,
post: {
title: 'Securing supergraphs',
views: null,
},
},
errors: [
{
message: 'Unauthorized field or type',
path: ['me'],
locations: [
{
line: 3,
column: 17,
},
],
extensions: {
code: 'UNAUTHORIZED_FIELD_OR_TYPE',
},
},
{
message: 'Unauthorized field or type',
path: ['post', 'views'],
locations: [
{
line: 8,
column: 19,
},
],
extensions: {
code: 'UNAUTHORIZED_FIELD_OR_TYPE',
},
},
],
}
Scenario 2: Accessing unauthorized field under the hood in order to resolve a public field
We have the subgraphs below that has a id
field as a key which is marked as authenticated
field. So in order to access this field, the client should be authorized. BUT, if it needs to be used under the hood to resolve another public
field, it is ok to use it.
# Product subgraoh
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5"
import: ["@key", "@authenticated"]
)
type Query {
product: Product
}
type Product @key(fields: "id") {
id: ID! @authenticated
name: String!
price: Int @authenticated
}
# Inventory subgraph
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5"
import: ["@key", "@authenticated"]
)
type Product @key(fields: "id") {
id: ID! @authenticated
inStock: Boolean!
}
In this query, there will be no unauth error. It can successfully resolve id
from Product subgraph then pass it to Inventory
to get inStock
. id
is protected field but not exposed to the public. So it is ok to use it internally in order to resolve a public field.
query {
product {
name
inStock
}
}
Scenario 3:
Accessing id
later on is not permitted still, so even if it is resolved and held, other than any public field, it will be nullified and errored with a path pointing to that private unauthorized field;
query {
product {
name
inStock
}
}
Will fail as;
{
data: {
product: {
id: null,
name: 'Couch',
},
},
errors: [
{
message: 'Unauthorized field or type',
path: ['product', 'id'],
locations: [
{
line: 4,
column: 19,
},
],
extensions: {
code: 'UNAUTHORIZED_FIELD_OR_TYPE',
},
},
],
}
Scenario 4: Interfaces
Let's say we have an interface as in Post
which is public but one of the implementations as in PrivateBlog
.
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5"
import: ["@authenticated"]
)
type Query {
posts: [Post!]!
}
type User {
id: ID!
username: String
posts: [Post!]!
}
interface Post {
id: ID!
author: User!
title: String!
content: String!
}
type PrivateBlog implements Post @authenticated {
id: ID!
author: User!
title: String!
content: String!
publishAt: String
allowedViewers: [User!]!
}
When we send the following query to the gateway, only allowedViewers
will fail while other fields of the same entity are still resolved.
Because they belong to Post
still while allowedViewers
belong to PrivateBlog
specifically. This is an important case because you should realize that id
, author
and title
there are still from PrivateBlog
type which is resolved under the hood.
query {
posts {
id
author {
username
}
title
... on PrivateBlog {
allowedViewers {
username
}
}
}
}
So the expected result is;
{
data: {
posts: [
{
id: '1',
author: {
username: 'john.doe',
},
title: 'Securing supergraphs',
allowedViewers: null,
},
{
id: '2',
author: {
username: 'jane.doe',
},
title: 'Running supergraphs',
allowedViewers: null,
},
],
},
errors: [
{
message: 'Unauthorized field or type',
extensions: {
code: 'UNAUTHORIZED_FIELD_OR_TYPE',
},
locations: [
{
column: 19,
line: 10,
},
],
path: ['posts', 'allowedViewers'],
},
],
}
Scenario 5: RBAC with @requiresScopes
Let's say we have the following subgraph;
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5"
import: ["@requiresScopes"]
)
type Query {
user(id: ID!): User @requiresScopes(scopes: [["read:others"]])
users: [User!]! @requiresScopes(scopes: [["read:others"]])
post(id: ID!): Post
}
type User {
id: ID!
username: String
email: String @requiresScopes(scopes: [["read:email"]])
profileImage: String
posts: [Post!]!
}
type Post {
id: ID!
author: User!
title: String!
content: String!
}
And we send the following query with a viewer that has read:others
only.
query {
user(id: "1") {
username
profileImage
email
}
}
email
field will never be resolved, because read:email
doesn't exist in the viewer's scopes;
{
data: {
user: {
username: 'john.doe',
profileImage: 'https://example.com/john.jpg',
email: null,
},
},
errors: [
{
message: 'Unauthorized field or type',
extensions: {
code: 'UNAUTHORIZED_FIELD_OR_TYPE',
},
path: ['user', 'email'],
locations: [
{
line: 6,
column: 19,
},
],
},
],
}
Gateways
Hive Gateway, Cosmo Router and Apollo Router implement those directives in the same way with some cosmetic differences such as the message in the error, the error code etc.
- Apollo Router and Cosmo Router only support these directives with JWT Auth
- Hive Gateway allows you to use these directives with a custom auth logic
In addition to these directives, Hive Gateway has another mode which is protect-all
, so instead of protecting the fields only with @authenticated
directive, Hive Gateway protects all fields and types but skips when @skipAuth
directive is added.