Build a secure identity and access-control layer for GraphQL APIs using request context, authentication, authorization, roles, permissions, and field-level access control.
Authentication vs Authorization
Security:FoundationConcept:AuthN vs AuthZ
Authentication and authorization solve two different problems.
Authentication answers: "Who is making this request?"
Authorization answers: "Is this authenticated user allowed to perform this operation?"
For example, a GraphQL request may contain an access token. The server validates the token and identifies the user as user-42. That is authentication.
The server then checks whether user-42 is allowed to update order order-100. That is authorization.
Do not confuse authentication with authorization. A successfully authenticated user does not automatically have permission to perform every operation.
For example, an application may have these users:
alice — regular customer
bob — support agent
carol — administrator
All three may authenticate successfully, but their permissions can be different.
GraphQL itself does not prescribe JWT, sessions, OAuth, cookies, or any particular authentication mechanism. Those are application and infrastructure decisions.
Two Security Questions
Authentication:Who are you?
Establishes the identity associated with a request.
Authorization:What are you allowed to do?
Determines whether the authenticated identity can access a resource or perform an operation.
Authentication and Authorization Flow
Key Takeaways
Authentication establishes identity.
Authorization determines permissions.
GraphQL does not prescribe a specific authentication mechanism.
Authentication success does not imply authorization success.
Knowledge Check
A user successfully presents a valid JWT but is not allowed to delete another user's account. Which security concern prevents the operation?
Authentication Through GraphQL Context
A common GraphQL architecture authenticates the request before resolvers execute and places the authenticated identity into the GraphQL context.
The exact token verification mechanism depends on the application. It could involve a signed JWT, an opaque session token, a gateway, or another identity provider.
The important architectural principle is that authentication should happen consistently rather than requiring every resolver to independently parse and verify credentials.
A context object can also expose an authorization service:
💡 The context is created once for the request and contains the authenticated user. Resolvers can use that request-scoped identity without reimplementing token extraction and verification. In a production system, authentication errors should use the application's structured error strategy.
Key Takeaways
Authenticate requests before resolver execution where practical.
Expose authenticated identity through request-scoped context.
Avoid duplicating token parsing across resolvers.
Context can expose request-specific security and application services.
Knowledge Check
Why is request context commonly used to expose the authenticated user?
Role-Based Authorization
Role-Based Access Control (RBAC) assigns permissions through roles.
For example:
CUSTOMER
SUPPORT
ADMIN
The schema might expose:
enum Role {
CUSTOMER
SUPPORT
ADMIN
}
type User {
id: ID!
role: Role!
}
A resolver can enforce a role requirement:
function requireRole(context, allowedRoles) {
if (!context.user) {
throw new Error("Authentication required");
}
if (!allowedRoles.includes(context.user.role)) {
throw new Error("Forbidden");
}
}
RBAC is easy to understand, but roles can become too coarse as an application grows.
For example, saying that every SUPPORT user can update every customer record may be too broad. Real applications often need resource-level or attribute-level authorization as well.
Example Roles
CUSTOMER:Own account and customer operations
Usually has permissions limited to resources belonging to the user.
SUPPORT:Customer support operations
May have access to customer records without administrative capabilities.
ADMIN:Administrative operations
May have broad management permissions depending on the application's policy.
Key Takeaways
RBAC assigns permissions through roles.
Role checks are simple and useful for many applications.
Roles can become too broad for resource-specific authorization.
Authorization should reflect actual business rules.
Knowledge Check
What is the main idea behind RBAC?
Resource-Level Authorization
Role checks alone are often insufficient. Many applications need to determine whether the authenticated user can access a specific resource.
Consider:
mutation UpdateProfile($id: ID!, $input: UpdateProfileInput!) {
updateProfile(id: $id, input: $input) {
id
name
}
}
Suppose the authenticated user is user-10, but the requested profile belongs to user-20.
A simple CUSTOMER role check would not be enough. The server must verify ownership:
const profile = await profileService.findById(id);
if (profile.ownerId !== context.user.id) {
throw new Error("Forbidden");
}
This type of authorization is especially important in multi-tenant applications where users from one organization must never access another organization's data.
Resource-Level Authorization
Key Takeaways
Authentication alone does not protect individual resources.
Resource ownership can be part of an authorization policy.
Multi-tenant systems require strong tenant isolation.
Authorization logic should be centralized enough to remain consistent.
Knowledge Check
Why is a role check alone insufficient for a customer updating a profile?
Field-Level Authorization
GraphQL makes field-level access particularly important because clients can request arbitrary combinations of fields allowed by the schema.
Consider:
type User {
id: ID!
name: String!
email: String!
salary: Float
}
Suppose administrators can see salary, but ordinary users cannot.
This demonstrates an important GraphQL security principle: authorization cannot be applied only to top-level operations.
If a user can access a User object through a permitted query but one field contains sensitive information, that field itself may require authorization.
However, putting large authorization blocks inside individual field resolvers can become difficult to maintain. Larger systems may use reusable policy functions, directives, middleware, schema wrappers, or dedicated authorization services.
The correct choice depends on framework capabilities and application complexity.
GraphQL exposes fields, not just endpoints
Because clients can select individual fields and nested relationships, sensitive-field authorization must be considered independently from top-level operation authorization.
Key Takeaways
Sensitive GraphQL fields may require their own authorization rules.
Top-level authorization does not automatically protect every nested field.
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.
Authorization Layers
Operation-level:Can this user call the operation?
Useful for broad restrictions such as administrator-only mutations.
Resource-level:Can this user access this resource?
Checks ownership, organization membership, or resource-specific policies.
Field-level:Can this user see this field?
Useful for sensitive information such as salary or private metadata.