Handling Authentication and Authorization in GraphQL

Ryan Chenkie

@ryanchenkie

  • Developer Advocate @ Auth0
  • Google Developer Expert
  • Angularcasts.io,
  • GraphQLWorkshops.com

Phenix

Phenix from Slack

Provisioning tools are centered around permissions

How do we add authentication and authorization?

However we want!

GraphQL doesn't have an opinion about auth

But that's not very helpful

Let's look at some ways it can be done

1. Wrapping Resolvers

2. Custom Directives

API auth needs to answer a few questions

  • Is the requested data private?
  • Does the request contain authentication/authorization information?
  • Is that information valid?

Getting users to prove their identity is another topic

Typical Auth in REST


  const authMiddleware = (req, res, next) => {
    if (userIsAuthenticated(req)) {
      next();
    } else {
      res.status(401).send("Sorry, you're not allowed here!");
  }

  app.get('/private-data', authMiddleware, (req, res) => {
    res.send(somePrivateData);
  });

We want something similar in GraphQL

But not this...


  const authMiddleware = ...

  app.use('/graphql', authMiddleware, graphqlHTTP({
    schema
  });

We need something that

  • Isn't a catch-all
  • Gives us info on the authenticated user
  • Allows us to handle auth errors appropriately

How do we get there?

1. Wrapping Resolvers

Two Steps to Auth

  • Verify authentication info is legit
  • Use it to make authorization decisions

First we need to verify authentication


  const jwt = require('express-jwt');
  const jwtDecode = require('jwt-decode');

  const jwtMiddleware = jwt({ secret: 'some-strong-secret-key' });

  const getUserFromJwt = (req, res, next) => {
    const authHeader = req.headers.authorization;
    req.user = jwtDecode(authHeader);
    next();
  }

  app.use(jwtMiddleware);
  app.use(getUserFromJwt);

Now we can use the payload in our resolvers


  const resolvers = {
    Query: {
      articlesByAuthor: (_, args, context) => {
        return model.getArticles(context.user.sub);
      }
    }
  }

We can also do authorization checks


  const resolvers = {
    Query: {
      articlesByAuthor: (_, args, context) => {
        const scope = context.user.scope;
        if (scope.includes('read:articles')) {
          return model.getArticles(context.user.id);
        }
      }
    }
  }

This can get a bit repetitive

We can wrap the resolver instead

A resolver which needs to check scopes


  const checkScopeAndResolve = (scope, expectedScope, controller) => {
    const hasScope = scope.includes(expectedScope);
    if (!expectedScopes.length || hasScope) {
      return controller.apply(this);
    }
  }

  const controller = model.getArticles(context.user.id);

  const resolvers = {
    Query: {
      articlesByAuthor: (_, args, context) 
        => checkScopeAndResolve(
             context.user.scope,
             ['read:articles'],
             controller
          );
    }
  }

Let's get smarter about checking the JWT

Check the JWT in the wrapper


  import { createError } from 'apollo-errors';
  import jwt from 'jsonwebtoken';

  const AuthorizationError = createError('AuthorizationError', {
    message: 'You are not authorized!'
  });

  const checkScopeAndResolve = (context, expectedScope, controller) => {
    const token = context.headers.authorization;
    try {
      const jwtPayload = jwt.verify(token.replace('Bearer ', ''), secretKey);
      const hasScope = jwtPayload.scope.includes(expectedScope);
      if (!expectedScopes.length || hasScope) {
        return controller.apply(this);
      }
    } catch (err) {
      throw new AuthorizationError();
    }
  }

2. Custom Directives

What if we want to limit access to specific fields?

We can use custom directives

Directives give our queries more power


  query Hero($episode: Episode, $withFriends: Boolean!) {
    hero(episode: $episode) {
      name
      friends @include(if: $withFriends) {
        name
      }
    }
  }

We can use custom directives on our server


  const typeDefs = `
    
    directive @isAuthenticated on QUERY | FIELD
    directive @hasScope(scope: [String]) on QUERY | FIELD

    type Article {
      id: ID!
      author: String!
      reviewerComments: [ReviewerComment] @hasScope(scope: ["read:comments"])
    }

    type Query {
      allArticles: [Article] @isAuthenticated
    }
  `;

Defining Custom Directives


  const directiveResolvers = {
    isAuthenticated(result, source, args, context) {
      const token = context.headers.authorization;
      // ...
    },
    hasScope(result, source, args, context) {
      const token = context.headers.authorization;
      // ...
    }
  };

  const attachDirectives = schema => {
    forEachField(schema, field => {
      const directives = field.astNode.directives;
      directives.forEach(directive => {
        ...
      });
    });
  };

github.com/chenkie/graphql-auth

With this pattern we get

  • A clean API
  • Possibility for per-field authorization checks
  • Resolver-level authentication checks

There are many ways to do auth in GraphQL

Try a few, see what works best, combine them if you want

However we want!

Thank you!

@ryanchenkie

bit.ly/graphql-auth

graphqlworkshops.com

Handling Authentication and Authorization in GraphQL

By Ryan Chenkie

Handling Authentication and Authorization in GraphQL

Auth for GraphQL can be tricky. It's easy to lock down the entire endpoint but this is often too much. How do we get more specific? Let's talk about the available options and best practices for auth in GraphQL. You'll come away knowing everything you need to get a working auth solution in place

  • 2,908