graphql role based authorization
Asked Answered
C

2

6

I'm new to GraphQL and going to build a solution using GraphQL.

Everything looks cool but just concerned on how to implement the role based authorization inside GraphQL server (I'm considering using GraphQL.js/ apollo server)

I will have a users table which contains all users. Inside the users table there's a roles field which contains the roles of the particular user. The queries and mutations will be granted based on the roles of the user.

How can I implement this structure?

THANKS!

Cautious answered 11/2, 2018 at 19:46 Comment(0)
K
5

I've recently implemented role based authorisation by using GraphQL Shield, I found that using that package was the simplest way to do it. Otherwise you could add custom schema directives, here's a good article on how to do that: https://dev-blog.apollodata.com/reusable-graphql-schema-directives-131fb3a177d1.

There are a few steps you need to take to setup GraphQL Shield:

1 - Write an authentication function, here's a rough example you'll want to be doing much more than this i.e using JWTs and not passing the id:

export const isAdmin = async ({ id }) => {
  try {
    const exists = await ctx.db.exists.User({
      id: userId,
      role: 'ADMIN',
    });

    return exists
  } catch (err) {
    console.log(err);
    return false
  }
}

2 - In the file where you export all of your mutations and queries add the check:

const resolvers = {
 ...your queries and mutations
}

const permissions = {
   Query: {
     myQuery: isAdmin
   }
}

export default shield(resolvers, permissions);

This will now the isAdmin function every time your Query is requested.

I hope that helps

Karalee answered 8/4, 2018 at 20:17 Comment(0)
L
18

For apollo server developers, there have generally been 3 ways to implement authorization in Graphql:

  1. Schema-based: Adding a directive to the graphql types and fields you want to protect

  2. Middleware-based: Adding middleware (code that runs before and after your graphql resolvers have executed). This is the approach used by graphql-shield and other authorization libraries built on top of graphql-middleware.

  3. Business logic layer: This is the most primitive but granular approach. Basically, the function that returns data (i.e. a database query, etc) would implement its own permissions/authorization check.

Schema-based

  1. With schema-based authorization, we would define custom schema directives and apply them wherever it is applicable.

Source: https://www.apollographql.com/docs/graphql-tools/schema-directives/

//schema.gql

directive @auth(
  requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION

enum Role {
  ADMIN
  REVIEWER
  USER
  UNKNOWN
}

type User @auth(requires: USER) {
  name: String
  banned: Boolean @auth(requires: ADMIN)
  canPost: Boolean @auth(requires: REVIEWER)
}

// main.js

class AuthDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    this.ensureFieldsWrapped(type);
    type._requiredAuthRole = this.args.requires;
  }

  visitFieldDefinition(field, details) {
    this.ensureFieldsWrapped(details.objectType);
    field._requiredAuthRole = this.args.requires;
  }

  ensureFieldsWrapped(objectType) {
    if (objectType._authFieldsWrapped) return;
    objectType._authFieldsWrapped = true;

    const fields = objectType.getFields();

    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const { resolve = defaultFieldResolver } = field;
      field.resolve = async function (...args) {
        // Get the required Role from the field first, falling back
        // to the objectType if no Role is required by the field:
        const requiredRole =
          field._requiredAuthRole ||
          objectType._requiredAuthRole;

        if (! requiredRole) {
          return resolve.apply(this, args);
        }

        const context = args[2];
        const user = await getUser(context.headers.authToken);
        if (! user.hasRole(requiredRole)) {
          throw new Error("not authorized");
        }

        return resolve.apply(this, args);
      };
    });
  }
}

const schema = makeExecutableSchema({
  typeDefs,
  schemaDirectives: {
    auth: AuthDirective,
    authorized: AuthDirective,
    authenticated: AuthDirective
  }
});

Middleware-based

  1. With middleware-based authorization, most libraries will intercept the resolver execution. The below example is specific to graphql-shield on apollo-server.

Graphql-shield source: https://github.com/maticzav/graphql-shield

Implementation for apollo-server source: https://github.com/apollographql/apollo-server/pull/1799#issuecomment-456840808

// shield.js

import { shield, rule, and, or } from 'graphql-shield'

const isAdmin = rule()(async (parent, args, ctx, info) => {
  return ctx.user.role === 'admin'
})

const isEditor = rule()(async (parent, args, ctx, info) => {
  return ctx.user.role === 'editor'
})

const isOwner = rule()(async (parent, args, ctx, info) => {
  return ctx.user.items.some(id => id === parent.id)
})

const permissions = shield({
  Query: {
    users: or(isAdmin, isEditor),
  },
  Mutation: {
    createBlogPost: or(isAdmin, and(isOwner, isEditor)),
  },
  User: {
    secret: isOwner,
  },
})

// main.js

const { ApolloServer, makeExecutableSchema } = require('apollo-server');
const { applyMiddleware } = require('graphql-middleware');
const shieldMiddleware = require('shieldMiddleware');

const schema = applyMiddleware(
  makeExecutableSchema({ typeDefs: '...', resolvers: {...} }),
  shieldMiddleware,
);
const server = new ApolloServer({ schema });
app.listen({ port: 4000 }, () => console.log('Ready!'));

Business logic layer

  1. With business logic layer authorization, we would add permission checks inside our resolver logic. It is the most tedious because we would have to write authorization-checks on every resolver. The link below recommends placing the authorization logic in the business logic layer (i.e. sometimes called 'Models' or 'Application logic' or 'data-returning function').

Source: https://graphql.org/learn/authorization/

Option 1: Auth logic in resolver

// resolvers.js

const Query = {
  users: function(root, args, context, info){
    if (context.permissions.view_users) {
      return ctx.db.query(`SELECT * FROM users`)
    }
    throw new Error('Not Authorized to view users')
  }
}

Option 2 (Recommended): Separating out authorization logic from resolver

// resolver.js

const Authorize = require('authorization.js')

const Query = {
  users: function(root, args, context, info){
    Authorize.viewUsers(context)
  }
}

// authorization.js

const validatePermission = (requiredPermission, context) => {
  return context.permissions[requiredPermission] === true
}

const Authorize = {
  viewUsers = function(context){
    const requiredPermission = 'ALLOW_VIEW_USERS'

    if (validatePermission(requiredPermission, context)) {
      return context.db.query('SELECT * FROM users')
    }

    throw new Error('Not Authorized to view users')
  },
  viewCars = function(context){
     const requiredPermission = 'ALLOW_VIEW_CARS';

     if (validatePermission(requiredPermission, context)){
       return context.db.query('SELECT * FROM cars')
     }

     throw new Error('Not Authorized to view cars')
  }
}
Lick answered 11/12, 2019 at 20:13 Comment(1)
There are some limitations to graphql-shield, in that it tends to block/kill the entire query if any single permission fails. For example, the available functionality is allow / deny. It (so far) hasn't been able to allowSome. For example, if some permissions pass and some fail, it can't selectively allow parts of the query to succeed.Lick
K
5

I've recently implemented role based authorisation by using GraphQL Shield, I found that using that package was the simplest way to do it. Otherwise you could add custom schema directives, here's a good article on how to do that: https://dev-blog.apollodata.com/reusable-graphql-schema-directives-131fb3a177d1.

There are a few steps you need to take to setup GraphQL Shield:

1 - Write an authentication function, here's a rough example you'll want to be doing much more than this i.e using JWTs and not passing the id:

export const isAdmin = async ({ id }) => {
  try {
    const exists = await ctx.db.exists.User({
      id: userId,
      role: 'ADMIN',
    });

    return exists
  } catch (err) {
    console.log(err);
    return false
  }
}

2 - In the file where you export all of your mutations and queries add the check:

const resolvers = {
 ...your queries and mutations
}

const permissions = {
   Query: {
     myQuery: isAdmin
   }
}

export default shield(resolvers, permissions);

This will now the isAdmin function every time your Query is requested.

I hope that helps

Karalee answered 8/4, 2018 at 20:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.