Skip to content
All essays
CraftMarch 31, 202411 min

GraphQL Authentication and Authorization

Implement secure authentication in GraphQL applications

Ü
Ümit Uz
Mobile & Full Stack Developer

Implementing proper authentication and authorization in GraphQL requires careful consideration at both server and client levels.

Authentication Patterns

JWT Authentication

Server setup:

javascript
import { ApolloServer } from '@apollo/server';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET;

const context = async ({ req }) => {
  const token = req.headers.authorization || '';
  let user = null;

  if (token) {
    try {
      user = jwt.verify(token.replace('Bearer ', ''), JWT_SECRET);
    } catch (err) {
      console.error('Invalid token');
    }
  }

  return { user };
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context
});

Authentication Middleware

javascript
const authMiddleware = (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new AuthenticationError('You must be logged in');
    }
    return resolver(parent, args, context, info);
  };
};

const resolvers = {
  Query: {
    me: authMiddleware((parent, args, context) => {
      return context.user;
    })
  }
};

Authorization

Role-Based Access Control

javascript
const hasRole = (role) => (resolver) => {
  return (parent, args, context) => {
    if (!context.user) {
      throw new AuthenticationError('Not authenticated');
    }
    if (!context.user.roles.includes(role)) {
      throw new ForbiddenError('Insufficient permissions');
    }
    return resolver(parent, args, context);
  };
};

const resolvers = {
  Mutation: {
    deleteUser: hasRole('admin')((parent, { id }, context) => {
      return database.deleteUser(id);
    })
  }
};

Custom Directives

graphql
directive @auth on FIELD_DEFINITION
directive @admin on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION

type Query {
  me: User! @auth
  users: [User!]! @admin
  settings: Settings! @hasRole(role: "admin")
}

Directive Implementation

javascript
import { defaultFieldResolver } from 'graphql';

const authDirective = (schema) => {
  const typeMap = schema.getTypeMap();

  Object.values(typeMap).forEach(type => {
    if (type.getFields) {
      const fields = type.getFields();
      Object.values(fields).forEach(field => {
        const directives = field.astNode.directives;
        const authDirective = directives?.find(d => d.name.value === 'auth');

        if (authDirective) {
          const { resolve = defaultFieldResolver } = field;
          field.resolve = async (parent, args, context, info) => {
            if (!context.user) {
              throw new AuthenticationError('You must be logged in');
            }
            return resolve(parent, args, context, info);
          };
        }
      });
    }
  });

  return schema;
};

Context Building

Building Context with User

javascript
const context = async ({ req, res }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  let user = null;
  if (token) {
    user = await getUserFromToken(token);
  }

  return {
    user,
    req,
    res,
    dataSources: {
      userAPI: new UserAPI({ user }),
      productAPI: new ProductAPI()
    }
  };
};

DataLoader with Auth

javascript
const context = async ({ req }) => {
  const user = await authenticateUser(req);

  return {
    user,
    loaders: {
      user: new DataLoader(async (ids) => {
        return await db.users.find({
          where: {
            id: { in: ids },
            // Only return users user has access to
            organizationId: user.organizationId
          }
        });
      })
    }
  };
};

Client-Side Auth

javascript
import { ApolloLink, setContext } from '@apollo/client';

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

Token Refresh

javascript
import { onError } from '@apollo/client/link/error';
import { fromPromise } from '@apollo/client';

let isRefreshing = false;
let pendingRequests = [];

const refreshToken = () => {
  if (!isRefreshing) {
    isRefreshing = true;
    return fromPromise(
      refreshAccessToken()
        .then(token => {
          pendingRequests.forEach(cb => cb(token));
          pendingRequests = [];
          return token;
        })
        .finally(() => {
          isRefreshing = false;
        })
    );
  }

  return fromPromise(
    new Promise(resolve => {
      pendingRequests.push(resolve);
    })
  );
};

const errorLink = onError(({ graphQLErrors, operation, forward }) => {
  if (graphQLErrors) {
    const isAuthError = graphQLErrors.some(
      err => err.extensions.code === 'UNAUTHENTICATED'
    );

    if (isAuthError) {
      return fromPromise(
        refreshToken()
      ).flatMap(token => {
        operation.setContext(({ headers = {} }) => ({
          headers: {
            ...headers,
            authorization: `Bearer ${token}`
          }
        }));
        return forward(operation);
      });
    }
  }
});

Protected Queries

Client-Side Guards

javascript
import { useQuery } from '@apollo/client';
import { useAuth } from './auth-context';

function ProtectedComponent() {
  const { user, loading } = useAuth();
  const { data, loading: queryLoading } = useQuery(GET_SENSITIVE_DATA, {
    skip: !user
  });

  if (loading || queryLoading) return <Loading />;
  if (!user) return <LoginPrompt />;

  return <SensitiveData data={data} />;
}

Higher-Order Component

javascript
import { useAuth } from './auth-context';

function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();

    if (loading) return <Loading />;
    if (!user) return <Navigate to="/login" />;

    return <Component {...props} user={user} />;
  };
}

const Dashboard = withAuth(({ user }) => {
  return <div>Welcome, {user.name}</div>;
});

Row-Level Security

User-Specific Data

javascript
const resolvers = {
  Query: {
    posts: (parent, args, context) => {
      return context.dataSources.postAPI.getPostsForUser(
        context.user.id
      );
    }
  }
};

Organization Filtering

javascript
class PostAPI extends RESTDataSource {
  async getPosts() {
    const response = await this.get('posts', {
      headers: {
        'X-User-ID': this.context.user.id,
        'X-Org-ID': this.context.user.organizationId
      }
    });
    return response.data;
  }
}

Security Best Practices

Input Validation

javascript
const resolvers = {
  Mutation: {
    updateUser: async (parent, { id, input }, context) => {
      // Validate user owns this account
      if (id !== context.user.id) {
        throw new ForbiddenError('Can only update own profile');
      }

      // Sanitize input
      const sanitized = sanitizeUserInput(input);

      return await database.updateUser(id, sanitized);
    }
  }
};

Rate Limiting

javascript
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/graphql', limiter);

Query Complexity Analysis

javascript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const complexityLimitRule = createComplexityLimitRule(1000);

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [complexityLimitRule]
});

Conclusion

Proper authentication and authorization are critical for GraphQL APIs. Implement defense in depth with multiple layers of security.

Enjoyed this essay? Share it.

Related essays

Next essay
Trees and Graphs: Hierarchical Data Structures