By default, Mercurius Auth runs in directive
mode. To define out-of-band external auth policies in a policy system, we must set the mode
to 'external'
. This is done at plugin registration.
app.register(mercuriusAuth, {
...,
mode: 'external',
policy: {
...
}
})
When enabled, External Policy mode supports the following auth policy features:
- Apply policies on GraphQL Object type fields
- Apply policies on GraphQL Object types
Let's say we want to implement a policy system such that only users with appropriate permissions can access specific fields within the GraphQL schema. In External Policy mode, we need to:
- Identify the fields to protect.
- Define the policy for each protected field.
- Apply this policy on each field.
Consider the following GraphQL schema:
type Message {
title: String
message: String
adminMessage: String
}
type Query {
messages: [Message]
message(title: String): Message
}
We want to protect the GraphQL schema such that:
- Only admins can access
Message.adminMessage
field. - Only users can access the
messages
Query. - Only users can access the
Message
Object Type.
Each user can have the following permissions:
user
admin
In this scenario, a users permission is passed in the GraphQL request header: x-user
.
To protect the Message.adminMessage
field, we define a GraphQL field auth policy using the Message
key for the type and adminMessage
key for the field:
const policy = {
Message: {
adminMessage: { requires: 'admin' }
}
}
To protect the messages
Query, we define a GraphQL field auth policy using the Query
key for the type and messages
key for the field:
const policy = {
Query: {
messages: { requires: 'user' }
}
}
To protect the Message
Object type, we define a GraphQL Object type auth policy using the Message
key for the type and __typePolicy
key to protect the entire type:
const policy = {
Message: {
__typePolicy: { requires: 'user' }
}
}
Putting it all together, we have the final policy:
const policy = {
Message: {
__typePolicy: { requires: 'user' },
adminMessage: { requires: 'admin' }
},
Query: {
messages: { requires: 'user' }
}
}
To apply the policy, we first need to register it and set the mode to external
:
app.register(mercuriusAuth, {
...,
mode: 'external',
policy: {
Message: {
__typePolicy: { requires: 'user' },
adminMessage: { requires: 'admin' }
},
Query: {
messages: { requires: 'user' }
}
}
})
Next, we need to set the auth context
in order to be able to access the users permissions:
app.register(mercuriusAuth, {
authContext (context) {
const permissions = context.reply.request.headers['x-user'] || ''
return { permissions }
},
})
Finally, we define the applyPolicy
function. The first argument passed to the applyPolicy
is the associated policy for the field. For example, for the Message.adminMessage
field, applyPolicy
this would be:
{ requires: 'admin' }
Therefore, we can use this to apply different policies for different fields (and types in the case of __typePolicy
). Using this information, we can apply the policy for a field and control the system behaviour accordingly for incoming GraphQL requests.
app.register(mercuriusAuth, {
...,
async applyPolicy (policy, parent, args, context, info) {
// For field `Message.adminMessage`
// policy: { requires: 'admin' }
// context.auth.permissions: ['user', 'admin'] - the permissions associated with the user (passed as headers in authContext)
return context.auth.permissions.includes(policy.requires)
},
...
})
When building a policy for a field, each key within the policy corresponds with the GraphQL type name. For example, if we wanted to protect a field on an Object Type:
type Message {
...
}
We would use the key: Message
:
{
Message: { ... }
}
Each key within the Object Type corresponds with the GraphQL field name on a type. For example, if we wanted to protect type field message
:
type Message {
title: String
message: String
}
We would use the key: message
:
{
Message: {
message: 'user'
}
}
Consider the following GraphQL schema:
type Message {
title: String
message: String
}
type Query {
message(title: String): Message
}
You can define an auth policy at plugin registration to protect the Query.message
field as follows:
app.register(mercuriusAuth, {
authContext (context) {
const permissions = context.reply.request.headers['x-user'] || ''
return { permissions }
},
async applyPolicy (policy, parent, args, context, info) {
return context.auth.permissions.includes(policy.requires)
},
mode: 'external',
policy: {
Query: {
message: { requires: 'user' }
}
}
})
Upon failure(s), an example GraphQL response will look like:
{
"data": {
"message": null
},
"errors": [
{
"message": "Failed auth policy check on message",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"message"
]
}
]
}
Building a policy for an entire type is initially similar to defining a policy for a field. Each key within the policy corresponds with the GraphQL type name. For example, if we wanted to protect an Object Type:
type Message {
...
}
We would use the key: Message
:
{
Message: { ... }
}
Then, to protect the entire GraphQL Object Type, we use the __typePolicy
key:
{
Message: {
__typePolicy: { requires: 'user' }
}
}
This also works alongside specific field policies on the type:
{
Message: {
__typePolicy: { requires: 'user'} ,
message: { requires: 'admin' }
}
}
For the following GraphQL schema:
type Message {
title: String
message: String
}
type Query {
message(title: String): Message
}
You can define an auth policy at plugin registration to protect the Message
object type using the reserved __typePolicy
field as follows:
app.register(mercuriusAuth, {
authContext (context) {
const permissions = context.reply.request.headers['x-user'] || ''
return { permissions }
},
async applyPolicy (policy, parent, args, context, info) {
return context.auth.permissions.includes(policy.requires)
},
mode: 'external',
policy: {
Message: {
__typePolicy: { requires: 'user' }
}
}
})
Upon failure(s), an example GraphQL response will look like:
{
"data": {
"message": {
"title": null,
"message": null
}
},
"errors": [
{
"message": "Failed auth policy check on title",
"locations": [
{
"line": 11,
"column": 3
}
],
"path": [
"message",
"title"
]
},
{
"message": "Failed auth policy check on message",
"locations": [
{
"line": 12,
"column": 3
}
],
"path": [
"message",
"message"
]
}
]
}