Designing a Reusable Authorization Layer
As the number of GraphQL operations grows, repeating authorization logic directly inside resolvers becomes difficult to maintain.
A reusable authorization service can centralize policies:
const authorization = {
canReadUser(actor, target) {
return actor.id === target.id || actor.role === "ADMIN";
},
canUpdateUser(actor, target) {
return actor.id === target.id || actor.role === "ADMIN";
},
canDeleteUser(actor, target) {
return actor.role === "ADMIN";
}
};
Resolvers then become simpler:
const resolvers = {
Mutation: {
deleteUser: async (_, { id }, context) => {
const target = await context.userService.findById(id);
if (!context.authorization.canDeleteUser(context.user, target)) {
throw new Error("Forbidden");
}
return context.userService.deleteUser(id);
}
}
};
This separation gives the application a central place to test authorization rules.
For larger organizations, authorization may become a dedicated policy engine or service. Policies can incorporate roles, resource ownership, organization membership, feature flags, geographic restrictions, subscription plans, or other business conditions.
A key principle is deny by default for sensitive operations. New fields and mutations should not accidentally become accessible simply because a developer forgot to add a permission check.
Can this user call the operation?Can this user access this resource?Can this user see this field?- Centralized authorization policies improve consistency.
- Authorization can operate at operation, resource, and field levels.
- Sensitive systems should generally deny access by default.
- Authorization rules should be tested independently from GraphQL transport.
What is a major benefit of centralizing authorization policies?