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
npm install @fire-shield/graphql@3.1.1 @fire-shield/core@3.1.1 graphqlQuick Start
1. Setup RBAC
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
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
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:
applyFireShieldDirectives(schema: GraphQLSchema, config?: {
hasPermission?: { directiveName?: string };
hasRole?: { directiveName?: string };
hasAnyPermission?: { directiveName?: string };
hasAllPermissions?: { directiveName?: string };
}): GraphQLSchemaExample:
import { applyFireShieldDirectives } from '@fire-shield/graphql';
let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = applyFireShieldDirectives(schema);Individual Directive Factories
You can also apply directives individually:
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:
import { fireShieldDirectiveTypeDefs } from '@fire-shield/graphql';
const typeDefs = `
${fireShieldDirectiveTypeDefs}
type Query {
users: [User!]! @hasPermission(permission: "user:read")
}
`;GraphQLRBACContext Interface
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:
interface MyContext extends GraphQLRBACContext {
db: Database;
logger: Logger;
}Available Directives
@hasPermission
Requires a specific permission:
type Query {
users: [User!]! @hasPermission(permission: "user:read")
}@hasRole
Requires a specific role:
type Mutation {
deleteUser(id: ID!): Boolean! @hasRole(role: "admin")
}@hasAnyPermission
Requires at least one of the specified permissions:
type Query {
posts: [Post!]! @hasAnyPermission(permissions: ["post:read", "post:write"])
}@hasAllPermissions
Requires all of the specified permissions:
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:
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):
type Query {
restrictedMessage: String @isDenied(permission: "data:sensitive")
}Context Requirements
The GraphQL context must include rbac and optionally user:
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:
{
extensions: {
code: 'UNAUTHENTICATED' | 'FORBIDDEN' | 'RBAC_NOT_CONFIGURED',
requiredPermission?: string,
requiredRole?: string,
requiredPermissions?: string[],
}
}Example error handler:
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
const schema = applyFireShieldDirectives(baseSchema, {
hasPermission: { directiveName: 'requiresPermission' },
hasRole: { directiveName: 'requiresRole' },
});Then use in schema:
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:
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:
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
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
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:
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:
type Subscription {
postCreated: Post! @hasPermission(permission: "post:subscribe")
adminNotifications: AdminNotification! @hasRole(role: "admin")
}TypeScript Support
Full type definitions included:
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
- Use directives for simple checks - Clean and declarative
- Use resolver logic for complex authorization - More flexibility
- Combine field-level and resolver authorization - Defense in depth
- Always validate on the server - Never trust client checks
- Cache permission results - Enable caching for performance
- Provide clear error messages - Help clients understand why access was denied
Next Steps
- tRPC Integration - Type-safe RPC with RBAC
- Core Concepts - Understanding permissions
- API Reference - Complete API documentation
