Part 5: Authentication and Authorization

Knowing Who Is Calling and What They Can Do

Most GraphQL tutorials add auth as an afterthought. In production microservices, it is a first-class concern that shapes the schema design, context factory, and resolver structure.

This part covers:

  • JWT verification in the Apollo Server context function

  • Field-level authorization using a resolver guard pattern

  • A schema directive (@auth, @hasRole) approach

  • Protecting subscriptions at connection time

  • Auth in the Python Strawberry service

JWT in the Apollo Context

Every request to the Apollo Server passes through the context function before any resolver runs. This is where I verify the JWT and attach the user to the context.

// src/auth.ts
import * as jose from 'jose';

export interface AuthUser {
  id: string;
  email: string;
  role: 'ADMIN' | 'EDITOR' | 'VIEWER';
}

const SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET ?? 'change-this-secret-in-production',
);

export async function verifyToken(token: string): Promise<AuthUser> {
  const { payload } = await jose.jwtVerify(token, SECRET, {
    algorithms: ['HS256'],
  });

  return {
    id: payload.sub as string,
    email: payload.email as string,
    role: payload.role as AuthUser['role'],
  };
}

export function extractBearerToken(authHeader: string | undefined): string | null {
  if (!authHeader?.startsWith('Bearer ')) return null;
  return authHeader.slice(7);
}

Key design decision: an invalid token sets user to null rather than throwing. This means public endpoints (queries that do not require auth) continue to work. Resolvers that require auth throw explicitly — which gives better error messages than a blanket 401 at the transport layer.

Field-Level Authorization: Resolver Guards

The simplest pattern is a guard function called at the top of each protected resolver:

Apply at the resolver level:

This pattern is explicit and easy to follow. Each protected resolver documents its own requirements at the top of the function body — there is no hidden magic.

Schema Directive Approach

A more declarative approach uses @auth and @hasRole directives in the SDL itself. This makes security requirements visible in the schema and enforces them via a mapper transformer rather than per-resolver calls.

Implement the directive transformer:

Apply the transformer when building the schema:

I prefer the resolver guard pattern for most projects because it is explicit and does not require schema transformer setup. The directive approach is worth it when you have many protected fields and want the security policy visible in the schema introspection.

Protecting Subscriptions

Subscriptions use a persistent WebSocket connection, not HTTP requests. The context is established at connection time, not per-message.

The TypeScript Apollo Client sends the auth token in connection params:

JWT Signing Utility

For development and testing, a minimal signing function:

In production, JWTs typically come from a dedicated identity service (not issued by the product service). The product service only verifies.

Python Strawberry: Auth with Permissions

In the Python Strawberry service (which becomes a subgraph in Part 6), authorization uses Strawberry's PermissionExtension:

Apply permissions to resolvers in the schema definition:

Build the Strawberry context from the FastAPI request:

Wire the context into the Strawberry FastAPI router:

Authorization Summary

Pattern
TypeScript
Python

Auth context

jose.jwtVerify in context factory

python-jose in get_context

Guard approach

requireAuth(ctx) / requireRole(ctx, ...)

PermissionExtension classes

Directive approach

@auth / @hasRole schema directives

n/a (Strawberry uses decorators)

Subscription auth

connectionParams.authToken verified on connect

Not applicable in this series

Token issuance

Separate identity service

Separate identity service

What's Next

Authentication and authorization are now handled end to end — from JWT verification in the context function to field-level guards in both TypeScript and Python.

In Part 6, we wire everything together with Apollo Federation v2 — the TypeScript product service and the Python analytics service become independent subgraphs, unified by an Apollo Router supergraph.

Last updated