HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
6 Topics
Chapter•170 Minutes

Authentication, Authorization and Context

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.

A useful request flow is:

Request
  ↓
Extract credentials
  ↓
Authenticate identity
  ↓
Create GraphQL context
  ↓
Execute operation
  ↓
Authorize requested action
  ↓
Business logic
  ↓
Data source

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.

Conceptually:

const context = async ({ req }) => {
  const token = extractToken(req);
  const user = await verifyToken(token);

  return {
    user,
    requestId: req.headers["x-request-id"]
  };
};

A resolver can then access the identity:

const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new Error("Authentication required");
      }

      return context.user;
    }
  }
};

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:

return {
  user,
  authorizationService,
  userService,
  requestId
};

This keeps resolver code focused on API coordination rather than authentication implementation details.

Authenticated GraphQL Context
const createContext = async ({ req }) => {
  const token = extractBearerToken(req);
  const user = token ? await verifyAccessToken(token) : null;

  return {
    user,
    requestId: req.headers["x-request-id"] ?? crypto.randomUUID()
  };
};

const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new Error("Authentication required");
      }

      return context.user;
    }
  }
};
💡 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");
  }
}

Then:

const resolvers = {
  Mutation: {
    deleteUser: (_, { id }, context) => {
      requireRole(context, ["ADMIN"]);
      return context.userService.deleteUser(id);
    }
  }
};

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 is resource-level authorization.

A more scalable policy might be:

authorization.canUpdateProfile(context.user, profile)

The policy can then consider multiple conditions:

  • Is the user the owner?
  • Is the user an administrator?
  • Is the account active?
  • Is the resource locked?
  • Does the user's organization own the resource?

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.

A resolver can enforce the policy:

const resolvers = {
  User: {
    salary: (user, _, context) => {
      if (context.user?.role !== "ADMIN") {
        throw new Error("Forbidden");
      }

      return user.salary;
    }
  }
};

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.
  • Reusable authorization policies reduce duplicated resolver logic.
  • Field-level security is especially important for sensitive data.
Knowledge Check

Why might a sensitive GraphQL field require its own authorization check?

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.

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.
Key Takeaways
  • 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.
Knowledge Check

What is a major benefit of centralizing authorization policies?