Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
19 Aug 2025 · 6 min read ·Article 50 / 125
Go

50 Role-based Authorization in GraphQL

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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

  1. Single Entry Point: GraphQL has only one endpoint (/graphql), unlike REST where each endpoint can be protected individually.
  2. Dynamic Queries: Clients can shape queries however they like; filtering fields via the schema is impossible without middleware.
  3. 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:

RoleQuery:usersQuery:reportsMutation:createPostField: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

graphql
 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):

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}
This example is for field-level only; you can extend it to the ObjectType and Query/Mutation levels.


6. Simulating 50 Roles

Imagine an enterprise Human Resource (HR) application with 50 departments, each with its own set of roles.

json
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:

MERMAID
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:

  1. The user’s token is extracted to obtain the role
  2. The role is matched against the permission matrix
  3. If there’s no match, the request is rejected (403 Forbidden)
  4. 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.

json
1{
2  "role": "Analyst_Accounting",
3  "permissions": ["read:reports", "export:spreadsheets"]
4}
Instead of hardcoding the 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)

typescript
 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:


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! 🚀

Related Articles

💬 Comments