Simple authorization layer for your GraphQL server powered by directives.
- Inspired by NestJS guards
- GraphQL server agnostic
- Typeable context and directive arguments
- Written in TypeScript
- Fully tested
yarn add graphql-guards
import { makeExecutableSchema } from '@graphql-tools/schema';
import { addGuards, Guard } from 'graphql-guards';
import resolvers from './resolvers';
const typeDefs = /* GraphQL */ `
directive @block on FIELD_DEFINITION | OBJECT
type BlockedData @block {
secret: String!
}
type PublicData {
publicField: String!
blockedField: String @block
}
type Query {
publicData: PublicData!
blockedData: BlockedData!
blockedQuery: String @block
}
`;
const blockGuard: Guard = {
name: 'block',
execute: (_directiveArgs) => async (_parent, _args, _context) => {
throw new Error('You will never access this field or type.');
},
}
let schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Here is your protected schema
schema = addGuards(schema, [blockGuard]);
A guard is basically a directive that can be applied on locations FIELD_DEFINITION
and OBJECT
. It means you can then use this directive on a query, a mutation, an object field, or a type definition.
The directive can take arguments that can be used in your guard execution logic.
Example
directive @simpleGuard on FIELD_DEFINITION | OBJECT
directive @guardWithArgs(
myArg: Int!,
argWithDefaultValue: String = "Default value"
) on FIELD_DEFINITION | OBJECT
directive @fieldOnlyGuard on FIELD_DEFINITION
directive @typeOnlyGuard on OBJECT
To protect your GraphQL API with guards, you have to transform your executable schema with the addGuards
function exposed by the module. Here is the prototype of the function :
addGuards(schema: GraphQLSchema, guards: Guard[]): GraphQLSchema
The Guard
interface is exposed by the module. Here is how it looks :
interface Guard<TContext = any, TDirectiveArgs = any> {
// This must be the name of the directive declared in your schema
name: string;
// You will write the guard's logic in this method
execute: (directiveArgs: TDirectiveArgs) => GraphQLFieldResolver<any, TContext, any, void>;
}
The execute
method of a Guard must return a GraphQL resolver. It's role will be to perform checks, and to throw an error when one of them fails to avoid the execution of the real resolver.
Example
directive @auth(
requiresAdmin: Boolean = false
) on FIELD_DEFINITION | OBJECT
interface Context {
auth?: {
isAdmin: boolean;
}
}
const authGuard: Guard<Context, { requiresAdmin: boolean }> {
name: 'auth',
execute: ({ requiresAdmin }) => async (_parent, _args, { auth }) => {
if (!auth) {
throw new Error('Unauthorized');
}
if (requiresAdmin && !auth.isAdmin) {
throw new Error('Forbidden');
}
}
}