This plugin provides a way to handle authorization/permissions checks throughout your schema.
Because GraphQL schemas are graphs and fields can be aliased in responses, knowing what data is accessed at the root a query can be very difficult. Using a traditional pattern of performing checks at the start of a request, or by introspecting the result of a request does not work well, since data may be queried through a complex set of relations, and the resulting response can have fields aliased to any other name.
The GiraphQL auth plugin tries to solve a number of common authorization patterns/problems:
Simple checks on any field in a schema (At the Query/Mutation level, or nested deep inside a
schema)
Checks that run before resolving any field of a specific type
Checks that run after resolving any field of a specific type
Defining reusable permissions that are used by multiple field on the same object
Granting permissions from a parent field to the objects/types it returns
yarn add @giraphql/plugin-auth
import SchemaBuilder from '@giraphql/core';import '@giraphql/plugin-auth';​const builder = new SchemaBuilder({plugins: ['GiraphQLAuth'],authOptions: {// when true (default) fields not covered by a permission check will return an Authorization errorrequirePermissionChecks: true,// when true (default) mutation fields will not be authorized unless they are protected by a// permission check defined directly on the mutation field (NOT from `defaultPermissionCheck`).explicitMutationChecks: true,},});
builder.queryType({fields: t => ({hello: t.string({permissionCheck: (parent, args, context) => {// only say hello to people who capitalize their namereturn name[0] === args[0].toUpperCase(),},args: {name: t.arg.string({ required: true }),},resolve: (parent, { name }) => `hello, ${name}`,}),}),});
This check will run before the resolver for Query.hello
runs and will return an authorization error for any request where the name is not capitalized.
As you add more fields to your schema, you may want to re-use the same permission checks on multiple fields:
builder.queryType({permissions: {user: (parent, context) => !!context.user,admin: async (parent, context) => {if (!context.user) {return false;}// permission checks can be asyncconst roles = await context.user.roles();​return roles.includes('Admin');},},fields: (t) => ({helloAdmin: t.string({// permissionCheck can be a permission name (striing)permissionCheck: 'admin',resolve: (parent) => `hello Admin`,}),helloUser: t.string({// permissionCheck can be an array of permission namespermissionCheck: ['user'],resolve: (parent) => `hello Admin`,}),hello: t.string({// permissionCheck can be a function that returns a boolean, or descriibe a set of permissions// required to resolve the field.permissionCheck: (parent, args, context) => {// only say hello to people who capitalize their nameif (args.name[0] === args[0].toUpperCase()) {// return boolean as a basic checkreturn false;}​// return name of a permission that is required to read this fieldreturn 'user';​// return a list of permissiion names to requiire multiple permissionsreturn ['user', 'admin']; // or { all: ['user', 'admin'] }​// permissionCheck can also return a `PermissionMatcher` object for more advanced checks// that depend on complex checks (See API section below for more details)return { any: ['admin', { all: ['user', 'helloAccess'] }] };},args: {name: t.arg.string({ required: true }),},resolve: (parent, { name }) => `hello, ${name}`,}),}),});
The permissions
option allows you to create reusable permission checks that can be referenced by permissionCheck
on any field on that type. Permissions defined using permissions
option do NOT work with extends
fields from @giraphql/plugin-extends
since those fields would have a different parent type.
If multiple fields of an object use the same permission, the result of that check will be cached, and the check function will only be called once. This caching will only apply for checks called with the same parent
object, so if the check exists on a type that is returned in a list, the check will be called for each object in that list.
Often most fields on a type will use the same permission checks. To make this a little simpler, you can set default permission checks for a type that will be applied to any fields that do not explicitly set a permission check.
builder.queryType({permissions: {user: (parent, context) => !!context.user,admin: (parent, context) => !!context.user && context.user.isAdmin(),},defaultPermissionCheck: 'user',fields: (t) => ({hello: t.string({// uses default user check from `defaultPermissionCheck`resolve: (parent, { name }) => `hello, World`,}),helloAdmin: t.string({// does NOT use the user check from defaultPermissionCheckpermissionCheck: 'admin',resolve: (parent, { name }) => `hello Admin`,}),}),});
There are often cases where either it is more efficient to do a permission check once in a parent resolver, or the context you need to determine if a request should be authorized is not available in a child resolver. The naive solution would be to simple do the check in the parent, and not have permission checks for you child resolvers. Unfortunately this is susceptible to creating problems down the road if there are new resolvers that expose the same type, but forget that they need to add an auth check for the children. To address these kinds of use cases, the auth plugin allows fields to define a set of permissions to grant to the returned child.
builder.queryType({fields: (t) => ({people: t.field({type: ['Person'],// always allow querying for users, but fields on the returned users still have permission// checks.permissionCheck: () => true,grantPermissions: (parent, { name }, context) => {// conditionally grants user and admin permissions for the returned personreturn {user: !!context.user,admini: context.user.isAdmin(),};},resolve: (parent, args, { Users }) => Users.getAll(),}),}),});​builder.objectType('Person', {fields: (t) => ({firstName: t.exposeString('firstName', {// check for `user` permission which may have been granted by the grantPermissions option on `Query.people`permissionCheck: 'user',}),email: t.exposeString('email', {// check for `admin` permission which may have been granted by the grantPermissions option on `Query.people`permissionCheck: 'admin',}),}),});
As your schema gets more complicated, some types may be reference by fields on a lot of different types. This can make it hard to keep all the permission checks for this popular type in sync and ensure that all fields have appropriate checks. To make this simpler there is a preResolveCheck
you can add to object types that allows you to define a check that will run before any resolver for that type.
builder.queryType({fields: (t) => ({people: t.field({type: ['Person'],resolve: (parent, args, { Users }) => Users.getAll(),}),}),});​builder.objectType('Person', {// Run before the resolver for Query.peoplepreResolveCheck: (context) => {if (!context.user) {return false;}​return {readUserFields: true,readEmail: context.user.isAdmin(),};},fields: (t) => ({firstName: t.exposeString('firstName', {permissionCheck: 'readUserFields',}),email: t.exposeString('email', {permissionCheck: 'readEmail',}),}),});
the preResolveCheck
only receives the context
object because the parent
and args will vary depending on which resolver is being called. It can return a map of authorizations the same way an auth check on a field can. This approach allows you to define all of the auth for your type in one place, and is the recommended path for applications where you want more ridged controls over your permission checks. The check function may return a boolean, or a map of permissions.
Because the preResolveCheck
only receives the context object, it will be run at most once per request and its result will be cached for subsequent fields of the same kind.
One thing to note about preResolveCheck
is that checks for all members of a union field, and all implementors of an interface will be run before fields that return a union or interface, since there is no way to determine the actual type before resolving. This can be disabled with the skipPreResolveOnInterfaces
and skipPreResolveOnUnions
options for the plugin, or by setting skipImplementorPreResolveChecks
on the interface or skipMemberPreResolveChecks
on the union.
Another option is to use postResolveCheck
s, which will run for each instance of a type after it has been resolved, and will work with interface and unions fields as well.
builder.interfaceType('Shape', {fields: (t) => ({// interface fields}),});​builder.objectType('Square', {interfaces: ['Shape'],isTypeOf: (parent) => parent.type === 'square',// Will execute for each square returned by Query.shapes and grand the readSquare permission// which is required for the size fieldpostResolveCheck: (parent, context, info) => {return { readSquare: true };},fields: (t) => ({size: t.float({permissionCheck: 'readSquare',resolve: ({ edgeLength }) => edgeLength ** 2,}),}),});​builder.queryFields((t) => ({shapes: t.field({type: ['Shape'],// always allow this resolver to executepermissionCheck: () => true,resolve: () => {return [{ type: 'square', edgeLength: 4 }];},}),}));
TODO