Skip to content

GraphQL Integration

Fire Shield provides GraphQL directives and middleware for field-level and resolver-level authorization in GraphQL servers.

Features

  • Custom directives (@hasPermission, @hasRole, @hasAnyPermission, @hasAllPermissions, @notDenied, @isDenied)
  • Field-level authorization
  • Type-safe context interface
  • Works with Apollo Server, GraphQL Yoga, and others
  • TypeScript support
  • Schema-first approach

Installation

bash
npm install @fire-shield/graphql@3.1.1 @fire-shield/core@3.1.1 graphql

Quick Start

1. Setup RBAC

typescript
import { RBAC } from '@fire-shield/core';

const rbac = new RBAC();
rbac.createRole('admin', ['user:*', 'post:*']);
rbac.createRole('editor', ['post:read', 'post:write']);
rbac.createRole('viewer', ['post:read']);

2. Add Directives to Schema

graphql
directive @hasPermission(permission: String!) on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION
directive @hasAnyPermission(permissions: [String!]!) on FIELD_DEFINITION
directive @hasAllPermissions(permissions: [String!]!) on FIELD_DEFINITION
directive @notDenied(permission: String!) on FIELD_DEFINITION
directive @isDenied(permission: String!) on FIELD_DEFINITION

type Query {
  posts: [Post!]! @hasPermission(permission: "post:read")
  users: [User!]! @hasPermission(permission: "user:read")
  adminStats: AdminStats! @hasRole(role: "admin")
}

type Mutation {
  createPost(input: CreatePostInput!): Post! @hasPermission(permission: "post:write")
  deletePost(id: ID!): Boolean! @hasPermission(permission: "post:delete")
  updateUser(id: ID!, input: UpdateUserInput!): User! @hasPermission(permission: "user:write")
}

type Post {
  id: ID!
  title: String!
  content: String!
  # Only editors and admins can see draft posts
  draft: Boolean! @hasPermission(permission: "post:edit")
}

3. Create GraphQL Server

typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { makeExecutableSchema } from '@graphql-tools/schema';
import {
  applyFireShieldDirectives,
  fireShieldDirectiveTypeDefs,
} from '@fire-shield/graphql';

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyFireShieldDirectives(schema);

const server = new ApolloServer({ schema });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const user = await getUserFromRequest(req);
    return { rbac, user };
  },
});

API

applyFireShieldDirectives(schema, config?)

Applies all Fire Shield directives to a schema in one call.

Parameters:

typescript
applyFireShieldDirectives(schema: GraphQLSchema, config?: {
  hasPermission?: { directiveName?: string };
  hasRole?: { directiveName?: string };
  hasAnyPermission?: { directiveName?: string };
  hasAllPermissions?: { directiveName?: string };
}): GraphQLSchema

Example:

typescript
import { applyFireShieldDirectives } from '@fire-shield/graphql';

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyFireShieldDirectives(schema);

Individual Directive Factories

You can also apply directives individually:

typescript
import {
  createHasPermissionDirective,
  createHasRoleDirective,
  createHasAnyPermissionDirective,
  createHasAllPermissionsDirective,
  createNotDeniedDirective,
  createIsDeniedDirective,
} from '@fire-shield/graphql';

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = createHasPermissionDirective()(schema);
schema = createHasRoleDirective()(schema);
schema = createHasAnyPermissionDirective()(schema);
schema = createHasAllPermissionsDirective()(schema);
schema = createNotDeniedDirective()(schema);
schema = createIsDeniedDirective()(schema);

fireShieldDirectiveTypeDefs

A ready-made string containing all directive type definitions. Include it in your typeDefs to avoid writing them by hand:

typescript
import { fireShieldDirectiveTypeDefs } from '@fire-shield/graphql';

const typeDefs = `
  ${fireShieldDirectiveTypeDefs}

  type Query {
    users: [User!]! @hasPermission(permission: "user:read")
  }
`;

GraphQLRBACContext Interface

typescript
import type { GraphQLRBACContext } from '@fire-shield/graphql';

interface GraphQLRBACContext {
  rbac: RBAC;       // Fire Shield RBAC instance
  user?: RBACUser;  // Current user with roles
}

Extend it with your own context properties:

typescript
interface MyContext extends GraphQLRBACContext {
  db: Database;
  logger: Logger;
}

Available Directives

@hasPermission

Requires a specific permission:

graphql
type Query {
  users: [User!]! @hasPermission(permission: "user:read")
}

@hasRole

Requires a specific role:

graphql
type Mutation {
  deleteUser(id: ID!): Boolean! @hasRole(role: "admin")
}

@hasAnyPermission

Requires at least one of the specified permissions:

graphql
type Query {
  posts: [Post!]! @hasAnyPermission(permissions: ["post:read", "post:write"])
}

@hasAllPermissions

Requires all of the specified permissions:

graphql
type Mutation {
  deletePost(id: ID!): Boolean!
    @hasAllPermissions(permissions: ["post:delete", "post:admin"])
}

@notDenied

Blocks access if the given permission is explicitly denied for the user:

graphql
type Query {
  sensitiveData: JSON! @notDenied(permission: "data:sensitive")
}

@isDenied

Only executes the field resolver if the given permission IS explicitly denied for the user (useful for returning restricted fallback data):

graphql
type Query {
  restrictedMessage: String @isDenied(permission: "data:sensitive")
}

Context Requirements

The GraphQL context must include rbac and optionally user:

typescript
const context = ({ req }) => ({
  rbac: myRBACInstance,
  user: {
    id: req.user.id,
    roles: req.user.roles,
    // Optional: direct permissions
    permissions: req.user.permissions,
  },
});

Error Handling

The directives throw GraphQLError with specific error codes:

typescript
{
  extensions: {
    code: 'UNAUTHENTICATED' | 'FORBIDDEN' | 'RBAC_NOT_CONFIGURED',
    requiredPermission?: string,
    requiredRole?: string,
    requiredPermissions?: string[],
  }
}

Example error handler:

typescript
const server = new ApolloServer({
  schema,
  formatError: (error) => {
    if (error.extensions?.code === 'FORBIDDEN') {
      return {
        message: 'You do not have permission to access this resource',
        extensions: error.extensions,
      };
    }
    return error;
  },
});

Advanced Usage

Custom Directive Names

typescript
const schema = applyFireShieldDirectives(baseSchema, {
  hasPermission: { directiveName: 'requiresPermission' },
  hasRole: { directiveName: 'requiresRole' },
});

Then use in schema:

graphql
directive @requiresPermission(permission: String!) on FIELD_DEFINITION
directive @requiresRole(role: String!) on FIELD_DEFINITION

type Query {
  users: [User!]! @requiresPermission(permission: "user:read")
}

Programmatic Permission Checks

You can also check permissions directly in resolvers:

typescript
const resolvers = {
  Query: {
    users: (_, __, context: GraphQLRBACContext) => {
      // Manual check if needed
      if (!context.rbac.hasPermission(context.user, 'user:read')) {
        throw new GraphQLError('Permission denied');
      }
      return fetchUsers();
    },
  },
};

Dynamic Authorization in Resolvers

For complex authorization logic:

typescript
const resolvers = {
  Mutation: {
    updatePost: async (parent, { id, input }, context: GraphQLRBACContext) => {
      const post = await db.post.findUnique({ where: { id } });

      // Users can only edit their own posts unless they're admin
      const isOwner = context.user?.id === post.authorId;
      const isAdmin = context.user?.roles.includes('admin');

      if (!isOwner && !isAdmin) {
        throw new GraphQLError('Access Denied', {
          extensions: { code: 'FORBIDDEN' },
        });
      }

      return await db.post.update({ where: { id }, data: input });
    },
  },
};

Usage with Apollo Server

typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { makeExecutableSchema } from '@graphql-tools/schema';
import {
  applyFireShieldDirectives,
  fireShieldDirectiveTypeDefs,
} from '@fire-shield/graphql';

const typeDefs = `
  ${fireShieldDirectiveTypeDefs}

  type Query {
    posts: [Post!]! @hasPermission(permission: "post:read")
    post(id: ID!): Post
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post! @hasPermission(permission: "post:write")
    deletePost(id: ID!): Boolean! @hasPermission(permission: "post:delete")
  }
`;

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyFireShieldDirectives(schema);

const server = new ApolloServer({ schema });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const user = await getUserFromToken(req.headers.authorization);
    return { rbac, user };
  },
});

console.log(`Server ready at ${url}`);

Usage with GraphQL Yoga

typescript
import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { makeExecutableSchema } from '@graphql-tools/schema';
import {
  applyFireShieldDirectives,
  fireShieldDirectiveTypeDefs,
} from '@fire-shield/graphql';

const typeDefs = `
  ${fireShieldDirectiveTypeDefs}

  type Query {
    posts: [Post!]! @hasPermission(permission: "post:read")
  }
`;

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyFireShieldDirectives(schema);

const yoga = createYoga({
  schema,
  context: async ({ request }) => {
    const token = request.headers.get('authorization');
    const user = await getUserFromToken(token);
    return { rbac, user };
  },
});

const server = createServer(yoga);
server.listen(4000, () => {
  console.log('Server is running on http://localhost:4000/graphql');
});

Field-Level Authorization

Protect specific fields within a type:

graphql
type User {
  id: ID!
  username: String!
  email: String!

  # Only visible to admins
  internalNotes: String @hasRole(role: "admin")

  # Only visible if user has permission
  privateData: JSON @hasPermission(permission: "user:private")

  # Only visible with explicit permission
  apiKeys: [APIKey!]! @hasPermission(permission: "user:api-keys")
}

Subscription Authorization

Protect GraphQL subscriptions:

graphql
type Subscription {
  postCreated: Post! @hasPermission(permission: "post:subscribe")
  adminNotifications: AdminNotification! @hasRole(role: "admin")
}

TypeScript Support

Full type definitions included:

typescript
import type { GraphQLRBACContext } from '@fire-shield/graphql';

interface MyContext extends GraphQLRBACContext {
  db: Database;
  logger: Logger;
}

const resolvers = {
  Query: {
    users: (_, __, context: MyContext) => {
      context.rbac.hasPermission(context.user, 'user:read');
      context.logger.info('Fetching users');
      return context.db.users.findMany();
    },
  },
};

Best Practices

  1. Use directives for simple checks - Clean and declarative
  2. Use resolver logic for complex authorization - More flexibility
  3. Combine field-level and resolver authorization - Defense in depth
  4. Always validate on the server - Never trust client checks
  5. Cache permission results - Enable caching for performance
  6. Provide clear error messages - Help clients understand why access was denied

Next Steps