Implementing GraphQL Authorization: A Practical Guide
- Share:
GraphQL authorization ensures users have the right permissions to access parts of your GraphQL API. It's important for controlling read, write, or update access based on user roles and permissions. Built-in solutions can be complex, especially as your app grows.
To simplify authorization, we’ll use Permit.io, which simplifies managing permissions as your app scales. We will build a simple task management app to demonstrate how to use this tool for authorization in a scalable way.
Let's begin!
The Problem with GraphQL’s Built-In Authorization Solutions
The built-in authorization solutions in GraphQL can handle basic scenarios, but they often encounter several issues as your application's complexity increases.
- As your application grows, maintaining built-in authorization logic can become unwieldy. The nature of these solutions can lead to inconsistencies and increased complexity.
- The built-in solutions provided by GraphQL make it difficult to execute consistent security policies across your application. They are black-boxed, making it impossible to configure them in depth, thus increasing the risk of misconfigured permissions and potential vulnerabilities.
- Developing and maintaining a custom authorization system is time-intensive, diverting your team’s focus from building core features and improving user experience.
To solve these issues, we will use Permit.io, which addresses these limitations by providing a more comprehensive and flexible solution to managing permissions:
- Simplified Management: Built-in solutions can become cumbersome as your application grows. Permit.io simplifies the process by centralizing your authorization logic.
- Enhanced Security: Enforce consistent security policies across your application, reducing the risk of misconfigured permissions.
- Time-Saving: Save time by leveraging Permit.io's ready-to-use features instead of building and maintaining your authorization system.
Using Permit allows you to ensure a secure, scalable, and efficient approach to GraphQL authorization, helping your team prioritize delivering exceptional features and experiences to your users rather than spending time developing and maintaining an authorization system from scratch.
Building a Real-World App Using Node.js SDK
This tutorial explains how to build a simple Task management app with different permissions. For example, users can create, delete, and update their posts.
Setup a Node.js project
To get started with the Apollo server with Permit.io, first create a Node.js project. To save time, we will use a Node.js starter template with GraphQL.
Make sure you have
Node.js
andnpm
installed on your machine. You can download them from the official Node.js website.Open up your code editor and clone the Node.js starter template by running the following code
git clone <https://github.com/Arindam200/node-apollo-starter.git>
This will create a new Nodejs project with the Apollo server.
After creating the project, navigate to the project directory by running the following command:
cd node-apollo-starter
Next, we'll install the required dependencies by running the following command:
npm install
Create aÂ
.env
 file in the root directory to store the secret keys for our projects.PERMIT_API_KEY=your_api_key
With that, our Node.js project setup is done.
Configure Permit.io:
- Create your first workspace
- Go to the Policy page, click
Create a Resource
, and add the following details - Create a new role under the policy section using the following details, and click save.
- Go to the Policy Editor. Check all the checkboxes as shown below, and save the changes.
- Now, let’s create a user. Go to the permit directory, click Add User, and enter the following details. Then, click save.
Setup the environment
- Get your environment API Key and add the environment key to the
.env
file - Install permit.io SDK:
npm install permitio
typeDefs: The GraphQL schema definitions are imported from
schema.js
.resolvers: Resolvers imported from
taskResolvers.js
to handle GraphQL operations.context: Async function to set up the context for each GraphQL operation. It retrieves user information from the request using Permit.io and makes it available in the context.
Server Setup: Start the Apollo Server instance on port 4001
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import Permit from 'permitio';
import taskResolvers from './resolvers/taskResolvers.js';
import typeDefs from './schema.js';
const { Permit: PermitClass }=Permit;
const permit=new PermitClass({
// don't forget to add key to the .env file
pdp: '<https://cloudpdp.api.permit.io>',
token: process.env.PERMIT_API_KEY,
});
const server=new ApolloServer({
typeDefs,
resolvers: taskResolvers,
context: async ({ req }) => {
const user=await permit.getUserFromRequest(req);
return { user, permit };
},
});
startStandaloneServer(server, {
listen: { port: 4001 },
}).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
This setup ensures that your Apollo Server is integrated with Permit.io for handling user authentication and authorization, providing a secure and scalable GraphQL API for managing tasks.
4. Create a new file named src/schema.js
and define a GraphQL schema that collects tasks with title
and description
In this schema:
- Task Type: Represents a task with
id
,title
, and optionaldescription
. - Query Type: Includes queries to fetch all tasks (
tasks
) and a specific task by itsid
(task
). - Mutation Type: Defines mutations for creating (
createTask
), updating (updateTask
), and deleting (deleteTask
) tasks.
import gql from 'graphql-tag';
const typeDefs=gql`
type Task {
id: ID!
title: String!
description: String
}
type Query {
tasks: [Task]
task(id: ID!): Task
}
type Mutation {
createTask(title: String!, description: String): Task
updateTask(id: ID!, title: String, description: String): Task
deleteTask(id: ID!): Boolean
}
`;
export default typeDefs;
This schema will serve as the foundation for your GraphQL API to manage tasks, allowing you to perform CRUD operations effectively.
5. Create a file for resolvers src/taskResolvers.js
and write logic for the API.
Query Resolvers (Query
object):
- tasks: Fetches all tasks. It uses
permit.check(user, 'read', 'task')
to verify if the user has permission to read tasks. - task: Fetches a specific task by ID. Like
tasks
, it checks permissions before returning the task.
Mutation Resolvers (Mutation
object):
- createTask: Creates a new task with the provided
title
anddescription
. Before adding to thetasks
array, it verifies if the user is authorized to create tasks. - updateTask: Updates an existing task identified by its
id
. It checks permissions (permit.check(user, 'update', 'task')
) and modifies the task'stitle
anddescription
if provided. - deleteTask: Deletes a task identified by its
id
. It ensures the user has the authorization to delete tasks (permit.check(user, 'delete', 'task')
) before removing it from thetasks
array.
Authorization (permit.check
):
permit.check(user, operation, resource)
is used to validate if the user (user
) has permission (operation
) to perform actions on a resource (task
).
Error Handling:
- Errors are thrown (
throw new Error(...)
) when tasks are not found (updateTask
,deleteTask
) or when authorization fails (permit.check
).
import { tasks } from '../models/task.js';
const taskResolvers = {
Query: {
// Fetch all tasks
tasks: async (_, __, { user, permit }) => {
// Check if user is authorized to read tasks
await permit.check(user, 'read', 'task');
return tasks;
},
// Fetch a specific task by ID
task: async (_, { id }, { user, permit }) => {
// Check if user is authorized to read tasks
await permit.check(user, 'read', 'task');
return tasks.find(task => task.id === id);
},
},
Mutation: {
// Create a new task
createTask: async (_, { title, description }, { user, permit }) => {
// Check if user is authorized to create tasks
await permit.check(user, 'create', 'task');
const newTask = { id: String(tasks.length + 1), title, description };
tasks.push(newTask);
return newTask;
},
// Update an existing task
updateTask: async (_, { id, title, description }, { user, permit }) => {
// Check if user is authorized to update tasks
await permit.check(user, 'update', 'task');
const task = tasks.find(task => task.id === id);
if (!task) throw new Error('Task not found');
if (title !== undefined) task.title = title;
if (description !== undefined) task.description = description;
return task;
},
// Delete a task by ID
deleteTask: async (_, { id }, { user, permit }) => {
// Check if user is authorized to delete tasks
await permit.check(user, 'delete', 'task');
const index = tasks.findIndex(task => task.id === id);
if (index === -1) throw new Error('Task not found');
tasks.splice(index, 1);
return true;
},
},
};
export default taskResolvers;
This setup encapsulates CRUD operations for tasks within GraphQL resolvers, enforcing authorization checks to control data access based on user permissions.
6. Create another file named src/task.js
to manage your task data. In this file, you will define a model (tasks
) that contains dummy task objects. This will allow you to read and manipulate task data effectively within your application.
The tasks
model will serve as a placeholder for task data, providing a simple way to mock task information for development and testing purposes. By defining this model, you can easily create, read, update, and delete (CRUD) task entries in a structured format.
Here is an example of how you might structure the tasks
model with some initial dummy data:
export const tasks = [
{ id: '1', title: 'Task 1', description: 'Description for task 1' },
{ id: '2', title: 'Task 2', description: 'Description for task 2' },
];
In this example, each task object contains an ID, title
, and description
. You can expand or modify this structure based on your application's specific requirements. This setup will facilitate the management of task data as you develop and test your application.
You can also check the complete example in the GitHub repository.
Conclusion
In this tutorial, we have learned how to set up Permit.io & configure Apollo Server in a basic Nodejs app, including schema definitions and resolver implementations. Following these instructions will help you manage permissions effectively and streamline your authorization logic.
Now you can enhance your applications' security by applying it to real use cases.
Implementing both ReBAC and ABAC is always recommended, as they provide more fine-grained authorization for your applications. We suggest that you continue reading our learning materials, which cover topics such as the differences between RBAC and ABAC and how to add ABAC to your application using Permit.io.
Want to learn more about implementing authorization? Have questions? Join our **Slack community to reach out to us**.
Written by
Arindam Majumder
DevRel @Pieces, Frontend Developer Technical Writer, Content Creator