GraphQL Authorization â Role-Based Access Control Patterns
In this tutorial, you will learn about Graphql Authorization. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL authorization controls what authenticated users can do and see, implementing role-based access control (RBAC) at the field, type, and query levels.
What You'll Learn
You will learn how to implement role-based authorization, field-level permissions, row-level security, policy-based authorization with CASL, and testing authorization rules.
Why Authorization Matters
Authentication tells you who the user is. Authorization tells you what they can do. Without proper authorization, any authenticated user could access other users' data, perform admin actions, or view sensitive fields. DodaTech's Durga Antivirus Pro has three roles: VIEWER (read-only dashboard), ANALYST (investigate threats), and ADMIN (configure system). Authorization checks ensure each role sees only what they need.
flowchart TB
A["User Request"] --> B["Authentication\n(who are you?)"]
B --> C["Authorization\n(what can you do?)"]
C --> D{"Role Check"}
D -->|"VIEWER"| E["Read threats,\nview dashboard"]
D -->|"ANALYST"| F["Investigate,\nquarantine devices"]
D -->|"ADMIN"| G["Configure system,\nmanage users,\ndelete threats"]
style D fill:#dbeafe,stroke:#2563eb
style E fill:#fef3c7,stroke:#d97706
style F fill:#fef3c7,stroke:#d97706
style G fill:#fef3c7,stroke:#d97706
Prerequisites: GraphQL authentication. Understanding of role-based access concepts.
Role Hierarchy
// Role hierarchy â higher roles inherit lower role permissions
const ROLES = {
VIEWER: 1,
ANALYST: 2,
ADMIN: 3,
};
function checkRole(user, requiredRole) {
if (!user) throw new AuthenticationError('Not authenticated');
if ((ROLES[user.role] || 0) < ROLES[requiredRole]) {
throw new ForbiddenError(`Requires ${requiredRole} role`);
}
}
const resolvers = {
Mutation: {
deleteThreat: (_, { id }, context) => {
checkRole(context.user, 'ADMIN');
return context.db.threats.delete(id);
},
investigateThreat: (_, { id }, context) => {
checkRole(context.user, 'ANALYST'); // ANALYST or ADMIN
return context.db.threats.investigate(id);
},
},
};
Field-Level Permissions
type User {
id: ID!
email: String!
displayName: String!
role: String!
# Only the user themselves or admins can see these
ssoToken: String
apiKeys: [APIKey!]
billingInfo: BillingInfo
# Only admins
lastLoginIp: String
loginHistory: [LoginEvent!]
}
const resolvers = {
User: {
// Owner or admin access
ssoToken: (parent, args, context) => {
if (context.user?.id === parent.id || context.user?.role === 'ADMIN') {
return parent.ssoToken;
}
return null; // Hide from unauthorized users
},
// Admin only
loginHistory: (parent, args, context) => {
if (context.user?.role !== 'ADMIN') {
return null;
}
return parent.loginHistory;
},
},
};
Row-Level Security (RLS)
// Users can only access their own data (unless admin)
const resolvers = {
Query: {
devices: (parent, args, context) => {
if (!context.user) throw new AuthenticationError('Login required');
if (context.user.role === 'ADMIN') {
// Admins can see all devices
if (args.userId) {
return context.db.devices.findByUserId(args.userId);
}
return context.db.devices.findAll();
}
// Regular users see only their own devices
return context.db.devices.findByUserId(context.user.id);
},
device: (parent, { id }, context) => {
if (!context.user) throw new AuthenticationError('Login required');
const device = context.db.devices.findById(id);
if (!device) throw new UserInputError('Device not found');
// Check ownership
if (device.userId !== context.user.id && context.user.role !== 'ADMIN') {
throw new ForbiddenError('You can only access your own devices');
}
return device;
},
},
};
Policy-Based Authorization with CASL
const { Ability, AbilityBuilder } = require('@casl/ability');
// Define permissions based on user role
function defineAbilitiesFor(user) {
const { can, cannot, build } = new AbilityBuilder(Ability);
if (user.role === 'ADMIN') {
can('manage', 'all'); // Full access
} else if (user.role === 'ANALYST') {
can('read', 'Threat');
can('update', 'Threat', { status: 'OPEN' }); // Only open threats
can('read', 'Device');
cannot('delete', 'Threat');
cannot('manage', 'User');
} else if (user.role === 'VIEWER') {
can('read', 'Threat', { severity: { $in: ['LOW', 'MEDIUM'] } }); // Limited visibility
can('read', 'Device', { userId: user.id }); // Own devices only
}
return build();
}
// Resolver using CASL
const resolvers = {
Query: {
threats: (parent, args, context) => {
const ability = defineAbilitiesFor(context.user);
let threats = context.db.threats.findAll();
// Filter based on permissions
return threats.filter(threat => ability.can('read', threat));
},
},
};
Conditional Field Exposure
const resolvers = {
Threat: {
// Conditionally expose fields based on permissions
internalNotes: (parent, args, context) => {
if (context.user?.role === 'ADMIN' || context.user?.role === 'ANALYST') {
return parent.internalNotes;
}
return null; // VIEWERs don't see internal notes
},
// Alternative: return a typed result with visibility info
visibility: (parent, args, context) => {
return {
level: context.user?.role || 'PUBLIC',
canEdit: context.user?.role === 'ADMIN',
canDelete: context.user?.role === 'ADMIN',
canShare: context.user?.role === 'ANALYST' || context.user?.role === 'ADMIN',
};
},
},
};
Common Mistakes
1. Checking Authorization Only at Query Level
Checking auth only in the root query resolver but not in nested resolvers leaves gaps. A user could access a device through a relation even if they shouldn't.
2. Hardcoding Role Checks in Every Resolver
Duplicating if (role !== 'ADMIN') throw... in 20 resolvers is error-prone. Use directive-based guards or a centralized checkRole utility.
3. Missing Row-Level Security
Role checks alone are not enough. A USER role user should not access another user's data. Always verify data ownership.
4. Not Testing Negative Cases
Tests that verify "admin can delete" are common. Tests that verify "viewer cannot delete" are rare â but more important. Test every role's denied actions.
5. Exposing Authorization Logic in Error Messages
"Threat not found" vs "You cannot access this threat" leaks information. Use consistent error messages that don't reveal whether a resource exists.
Practice Questions
- What is the difference between authentication and authorization?
- How do you implement field-level permissions?
- What is row-level security?
- What is CASL and how does it help?
- Why should you avoid revealing existence in authorization errors?
Answers:
- Authentication verifies identity (who you are via JWT). Authorization verifies permissions (what you can do via roles/policies).
- In the resolver for each field, check
context.user.roleand return null or throw ForbiddenError if the user lacks permission for that field. - Row-level security means users can only access database rows they own. A user should only see their own devices unless they are an admin.
- CASL (CASL.js) is a JavaScript library for defining and checking abilities/permissions. It provides a declarative way to define who can do what on which resources.
- Returning "Threat not found" vs "You cannot access this threat" tells an attacker whether a threat exists. Use consistent messages regardless of the actual reason.
Challenge: Design a complete authorization system for DodaTech's multi-tenant API. Implement role-based access (ADMIN, ANALYST, VIEWER) with role inheritance, field-level permission directives (@auth(requires: ROLE)), row-level security for user-owned data, team-level access for collaborative investigations, and use CASL for policy-based authorization.
FAQ
Mini Project
Implement a complete authorization system for DodaTech. Define roles (VIEWER, ANALYST, ADMIN) with hierarchy and inheritance. Implement field-level @auth directive, row-level security for user-owned data, CASL policy-based permissions, team-based access for enterprise accounts, and comprehensive tests for every role's allowed and denied actions.
What's Next
| Topic | Description |
|---|---|
| Federation | Distributed GraphQL for Microservices |
| Security | Depth limiting, cost analysis |
| Testing | Testing GraphQL APIs |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro