50 Role-based Authorization in GraphQL
title: “50 Role-based Authorization in GraphQL: Taking Security & Scalability to the Next Level” date: 2024-06-26 author: “Ricky Pratama” tags: [graphql, authorization, security, backend, graphql-security]
“Authorization isn’t just about who can access what — it’s also about how you build the best possible controls for your team and your product.”
Over the past few years, backend development has increasingly shifted toward an API-first approach, and GraphQL has emerged as the headline star. While GraphQL’s flexibility brings enormous benefits, its authorization side remains a major challenge — especially as applications grow and access-control requirements increase. In this article, I want to share my experience and some best practices for implementing role-based authorization in GraphQL — even, if needed, scaling all the way up to 50+ roles!
1. Why Does Role-based Authorization Matter?
Before we dive deep into the implementation, why exactly is role-based authorization so vital, especially in GraphQL-based applications?
- Query Flexibility: GraphQL lets clients request any data they want; without authorization, the risk of data leakage is high.
- Granular Access Control: Modern businesses often have complex scenarios, such as different user roles, delegation across departments, and so on — which means the rules have to be extremely granular.
- Auditing & Compliance: Organizations need to record who accessed what for auditing and compliance purposes (e.g., GDPR, HIPAA).
2. Common Role-based Authorization Schemes
The role types we usually run into are as simple as Admin, User, and Guest. However, in larger businesses, roles can get very specific:
- Product Manager
- Data Analyst
- Sales Executive
- Junior Engineer
- Vendor Operator
- and so on — easily reaching 20, 30, or even 50 roles!
Each role requires different access privileges, queries, and even mutations.
3. The Challenges in GraphQL
- Single Entry Point: GraphQL has only one endpoint (
/graphql), unlike REST where each endpoint can be protected individually. - Dynamic Queries: Clients can shape queries however they like; filtering fields via the schema is impossible without middleware.
- Nested Authorization: In a GraphQL query, a single request can pull relations across several data types at once.
So how do we handle it when business roles reach 50 or more? Let’s break the steps down one by one.
4. Implementation Strategy: Role-permission Matrix
I like to use a Role-permission Matrix — a mapping table that defines each role’s access to resources, both at the query level and the field level.
A simple example:
| Role | Query:users | Query:reports | Mutation:createPost | Field:user.email |
|---|---|---|---|---|
| Admin | ✅ | ✅ | ✅ | ✅ |
| User | ❌ | ❌ | ✅ | ❌ |
| Manager | ✅ | ✅ | ❌ | ✅ |
| Analyst | ❌ | ✅ | ❌ | ❌ |
| … | … | … | … | … |
As a backend engineer, you can store it in the form of:
- Hardcoded configuration (a JS/TS/JSON/YAML file)
- A database table (for something more dynamic)
5. Implementation with Middleware / Directive
One of the most flexible ways to implement role-based authorization in GraphQL is through a custom directive (@auth(role: ...)) that you can easily attach to the schema or to specific fields.
Example Schema with a Directive
1type User {
2 id: ID!
3 name: String!
4 email: String! @auth(role: ["Admin", "Manager"])
5}
6
7type Query {
8 users: [User]! @auth(role: ["Admin", "Manager"])
9 reports: [Report]! @auth(role: ["Admin", "Analyst"])
10}Implementing the Directive in Apollo Server (TypeScript):
1import { SchemaDirectiveVisitor } from 'apollo-server-express';
2import { defaultFieldResolver, GraphQLField } from 'graphql';
3
4class AuthDirective extends SchemaDirectiveVisitor {
5 visitFieldDefinition(field: GraphQLField<any, any>) {
6 const { resolve = defaultFieldResolver } = field;
7 const { role } = this.args;
8 field.resolve = async function (...args) {
9 const context = args[2];
10 if (!context.user) throw new Error('Not authenticated');
11 if (!role.includes(context.user.role)) {
12 throw new Error('Not authorized');
13 }
14 return resolve.apply(this, args);
15 };
16 }
17}6. Simulating 50 Roles
Imagine an enterprise Human Resource (HR) application with 50 departments, each with its own set of roles.
1[
2 { "role": "Admin", "permissions": ["*"] },
3 { "role": "HR_Manager", "permissions": ["users", "reports"] },
4 { "role": "Recruiter", "permissions": ["candidates", "interviews"] },
5 ...
6 { "role": "Sales_Executive", "permissions": ["salesData"] },
7 { "role": "Engineer_Backend_Lead", "permissions": ["deploymentLogs"] }
8 // up to 50!
9]For example, users can only be accessed by Admin & HR_Manager, while deploymentLogs is only accessible to Engineer_Backend_Lead.
7. The Authorization Flow on a GraphQL Request
Let’s visualize the authorization process for a GraphQL request with 50 different roles:
flowchart TD
A[Client: Send GraphQL Request] --> B[Parse JWT Token]
B --> C{Valid User?}
C -- No --> Z[Return 401]
C -- Yes --> D[Extract User Role]
D --> E[Matching pada Role-permission Matrix]
E -- Allowed --> F[Proceed Resolver]
E -- Denied --> Y[Return 403]
F --> G[Return Data]
On each request:
- The user’s token is extracted to obtain the role
- The role is matched against the permission matrix
- If there’s no match, the request is rejected (403 Forbidden)
- If it matches, it proceeds to the resolver and returns the data
8. A Scalable Approach for 50++ Roles
a. Use Permission Patterns, Not Just Roles
Roles can keep growing; a best practice is to break a role down into a permission group.
1{
2 "role": "Analyst_Accounting",
3 "permissions": ["read:reports", "export:spreadsheets"]
4}role, the system can check the exact permission.b. Abstraction & Policy Layer
Use a policy engine such as Casbin or OPA to manage the mapping of roles & permissions.
c. Caching the Role Matrix
Hitting the DB on every request is inefficient. Caching the permission matrix in memory (e.g., Redis or a simple in-memory store) will greatly help performance.
9. Authorization Middleware Example (Express + Apollo)
1const rolePermissionMatrix = {
2 Admin: ['users', 'reports', 'logs'],
3 Analyst: ['reports'],
4 Engineer: ['logs']
5}
6
7// context.user.role is obtained from the decoded JWT
8const authorizationMiddleware = (resolverName: string) => {
9 return (parent, args, context, info) => {
10 const userRole = context.user.role;
11 const allowedPermissions = rolePermissionMatrix[userRole] || [];
12 if (!allowedPermissions.includes(resolverName)) {
13 throw new Error("Forbidden: No access");
14 }
15 return resolver(parent, args, context, info);
16 }
17}
18
19// usage in a resolver
20const resolvers = {
21 Query: {
22 users: authorizationMiddleware('users')(async (parent, args, context) => {
23 // ...
24 }),
25 reports: authorizationMiddleware('reports')(async (parent, args, context) => {
26 // ...
27 }),
28 }
29}
1const rolePermissionMatrix = {
2 Admin: ['users', 'reports', 'logs'],
3 Analyst: ['reports'],
4 Engineer: ['logs']
5}
6
7// context.user.role is obtained from the decoded JWT
8const authorizationMiddleware = (resolverName: string) => {
9 return (parent, args, context, info) => {
10 const userRole = context.user.role;
11 const allowedPermissions = rolePermissionMatrix[userRole] || [];
12 if (!allowedPermissions.includes(resolverName)) {
13 throw new Error("Forbidden: No access");
14 }
15 return resolver(parent, args, context, info);
16 }
17}
18
19// usage in a resolver
20const resolvers = {
21 Query: {
22 users: authorizationMiddleware('users')(async (parent, args, context) => {
23 // ...
24 }),
25 reports: authorizationMiddleware('reports')(async (parent, args, context) => {
26 // ...
27 }),
28 }
29}10. Conclusion
In enterprise scenarios, role-based authorization in GraphQL isn’t just a “nice-to-have” — it’s a must-have, especially once roles reach the dozens or even hundreds. By using the concepts of a role-permission matrix, directives, and a policy engine, our API architecture becomes scalable, maintainable, and ensures security and compliance for the business.
Remember: GraphQL is powerful, but it has to be wrapped in a security mindset, especially when it comes to roles and permissions!
Further Reading & Tools:
- Apollo Server Authorization Doc
- GraphQL Auth Directive Example
- Casbin — Policy engine for authorization
Hopefully the insights and best practices above can help you build new GraphQL APIs that are far more scalable, secure, and ready to take on enterprise challenges with 50+ roles! 🚀