Use custom directives to protect your graphql apis

David He
codeburst
Published in
6 min readFeb 18, 2018

Authentication is required for apps that not only have public api endpoints but also authenticated ones that are accessible to users with certain level of access. In REST world, we will normally attach authentication bit (token verification) to the middleware before requests hitting the actual route.

Take express for instance, we can use passport to verify the token that is set on request header. If JWT token contains expected scope, we will let the request through to route handler to process or otherwise rejects it with proper errors.

But, can we follow this pattern on our Graphql server?

Well, the answer is — it really depends.

In the following section, I will show you how you can implement authentication on your graphql server using middleware pattern.

Please note, I will only provide a high level overview and for the complete working example, please refer to https://github.com/jessedvrs/graphql-passport-example.

Auth your apis via express middleware pattern

I will use passport to give us a helping hand in authentications. Firstly, let’s code the authentication strategy:

Next, add auth middleware:

Finally, register middlewares:

const app = express();
app.use(authMiddleware);
app.use('/api/graphql', graphqlMiddleware);

Voila! Now you are able to retrieve user object that is available in your resolver:

export default {
cakes: {
type: new GraphQLList(CakeType),
resolve: (_, args, context) => {
if (!context.user) {
throw new Error('Only users can eat cakes.');
}
return [{flavour: 'red-velvet'}, {flavour: 'mocca'}];
},
},
};

It gets the job done for sure but it lacks flexibility and has downsides:

  • All apis must be authenticated against callers even though you have public apis since auth is applied to middleware
  • Scope check cannot be done once — you need to do it in route handlers where certain level of access is required
  • Could be tricky to implement granular scope check on specific fields in type

With custom directives, you can easily resolve those issues mentioned above.
Keep reading on!

Note: I assume you already have a basic understanding of how directives work in graphql. If that sounds unfamiliar to you, make sure you take time to read the post below to wrap your head around this concept.

graphql directives

The custom directives way

In this section, I will build a simple products supply server to illustrate my way of authentication implementation via custom directives.

Essentially, I will break the implementations into two parts:

  • Schema
  • Directive and Query/Mutation resolvers

Let’s start!

Add schema

First thing first, let’s define our two custom directives:

directive @isAuthenticated on QUERY | FIELD
directive @hasScope(scope: [String]) on QUERY | FIELD

Two things need to be pointed out:

  • QUERY | FIELD indicates that this directive can be applied to both field and query level.
  • In hasScope directive, it takes scope as a parameter which is an array of scopes in string representation.

Now, we have required directives defined. Great!

Next thing is about how and where we can use them. In this demo app, we have three query operations and one mutation operation.

  • allProductBySupplier
  • product
  • addProduct
  • suppliers

We will make allProductBySupplier, product and addProduct authenticated while leave suppliers public.

type Query {
allProductsBySupplier: [Product] @isAuthenticated
product: Product @isAuthenticated
suppliers: [Supplier]
}
type Mutation {
addProduct(input: ProductInput!): Product @hasScope(scope: [“add:product”])
}

As you can see, @isAuthenticated is applied to the non-public queries on query level.

For mutation, we require users who want to add product must have add:product scope. Similarly, we will also restrict who can read rating from product to only users who have read:rating scope.


type Product {
id: ID!
supplierId: ID!
sku: String
qty: Int
price: Int
parrot: String
rating: Int @hasScope(scope: [“read:rating”])
}

Here is the complete schema:

directive @isAuthenticated on QUERY | FIELD
directive @hasScope(scope: [String]) on QUERY | FIELD
type Product {
id: ID!
supplierId: ID!
sku: String
qty: Int
price: Int
parrot: String
rating: Int @hasScope(scope: ["read:rating"])
}
type Supplier {
id: ID!
name: String!
}
input ProductInput {
supplierId: ID!
sku: String!
qty: Int!
price: Int!
parrot: String!
rating: Int!
}
type Query {
allProductsBySupplier: [Product] @isAuthenticated
product: Product @isAuthenticated
suppliers: [Supplier]
}
type Mutation {
addProduct(input: ProductInput!): Product @hasScope(scope: ["add:product"])
}

Schema is ready now! let’s walk into next square — build query resolvers!

Add Resolvers

We will begin with custom directive resolvers — isAuthenticated and hasScope.

The idea here is we will want to read the Jwt token from authorization header set by the client and decode it to verify.

If it is not compromised, we then set the decoded value (user in our case) to context which will be available in all resolvers — i.e query resolvers.

Once we have the user set on context, next thing is to trigger next which is our query resolver to resolve each field.

In the case of invalid token, we should simply reject the requests by throwing errors.

isAuthenticated is implemented as follow:

isAuthenticated: (next, source, args, context) => {
const token = context.headers.authorization;
if (!token) {
throw new AuthorizationError({
message: "You must supply a JWT for authorization!"
});
}
try {
const decoded = jwt.verify(
token.replace("Bearer ", ""),
process.env.JWT_SECRET
);
context.user = decoded;
return next();
} catch (err) {
throw new AuthorizationError({
message: "You are not authorized."
});
}
};

hasScope behaves similarly to isAuthenticatedexcept that it does not need to set the user on context:

hasScope: (next, source, args, context) => {
const token = context.headers.authorization;
const expectedScopes = args.scope;
if (!token) {
throw new AuthorizationError({
message: 'You must supply a JWT for authorization!'
});
}
try {
const decoded = jwt.verify(
token.replace('Bearer ', ''),
process.env.JWT_SECRET
);
const scopes = decoded.scope.split(' ');
if (expectedScopes.some(scope => scopes.indexOf(scope) !== -1)) {
return next();
}
} catch (err) {
return Promise.reject(
new AuthorizationError({
message: `You are not authorized. Expected scopes: ${expectedScopes.join(
', '
)}`
})
);
}
}

Query and mutation resolvers are not fancy and full code is given below:

let PRODUCTS = require('./products');
let SUPPLIERS = require('./suppliers');
const allProductsBySupplier = (obj, args, ctx) => PRODUCTS.filter(p => p.supplierId === Number(ctx.user.sub));const addProduct = (obj, args, ctx) => {
const id = PRODUCTS.length + 1;
PRODUCTS.push({
...args.input,
id
});
return PRODUCTS.slice(-1)[0];
};
const product = (obj, args, ctx) => PRODUCTS.find(p => p.id === args.productId);const suppliers = (obj, args, ctx) => SUPPLIERS;module.exports = {allProductsBySupplier, product, suppliers, addProduct};

Glue all together

Up to this point, we have everything in place to protect our apis. The last bit is wire all these up into graphql server.

In the newer version of graphql-tools, makeExecutableSchema accepts one more parameter called directiveResolvers that allows us to attach custom directive resolvers to schema.


const resolvers = {
Query: {
allProductsBySupplier,
product,
suppliers
},
Mutation: {
addProduct
}
};
const schema = makeExecutableSchema({
typeDefs,
resolvers,
directiveResolvers
});

Demo

Demo time! We will use node-fetch to make our requests to the app server.

Copy token from section Supplier 1 JWT with ‘read:rating’ scope and paste it into Authorization header in client.js.

Start the server by running npm run dev and once server is up and running, go to terminal and run node client.js.

You should be able to see a list of products with rating filled up if token is valid.

Take your time and play with it to find more fun!

Last note

Before I let you go, there is one important thing I would like to point out — flow of resolvers execution

Recall how we use isAuthenticated:

type Query {
allProductsBySupplier: [Product] @isAuthenticated
}

It is applied to query level and will run first before allProductsBySupplierin the above example.

This is why we need to call next upon successful token verification to execute next resolver.

hasScope on the other hand is attached to the field:

type Product {

rating: Int @hasScope(scope: [“read:rating”])

}

Therefore, it will be run later than query resolvers in which case rating will be returned from the query resolver to hasScopewhere it will be either set to null or retained depending on the required scope of that user.

Conclusion

As you can see, have your apis protected through custom directive is more flexible than express middlware approach. Apart from this, custom directives also allow you to do more cool stuff such as capitalize the field value before it reaches query resolvers.

If you are interested in seeing potential power of custom directive, check Custom directives

Credits

Special thanks to Ryan Chenkie who gave the talk on how to do auth in graphql.

Published in codeburst

Bursts of code to power through your day. Web Development articles, tutorials, and news.

Written by David He

Senior Analyst, Cloud Engineer @National Australia Bank. Basketball is treated as the same level of importance as coding in my life.

Responses (2)

What are your thoughts?