About Page Schmidt
Page is a Principal Consultant on UDig's Software team.
This site uses cookies to enhance your browsing experience and deliver personalized content. By continuing to use this site, you consent to our use of cookies.
COOKIE POLICY
GraphQL has continued to increase in popularity since its public release by Facebook in 2015. Major companies like Netflix, PayPal, and Airbnb have enthusiastically adopted the REST-alternative and sung its praises. Here at UDig, we’ve seen it adopted by many of our clients, along with GraphQL Shield + AuthO, and have even included it as part of our upcoming training program for new hires.
One of the primary benefits of GraphQL is that clients have more granular control over the data they request. Instead of relying on backend developers to define the structure and content of API responses, frontend developers can build clients that query the API for just the fields that they need (and even retrieve multiple resources with a single request).
One challenge that comes with this granular control is making sure that users can only retrieve data they are authorized to see. Since there can be multiple routes in the graph to a single resource type, developers need to make sure there are no gaps that unauthorized users can gain access to.
This post aims to describe how to begin securing your GraphQL API with Auth0 and GraphQL Shield.
Below are the key technologies that we will leverage to build our Auth0-protected GraphQL API:
For this exercise, we will build a GraphQL API that stores data about several fictional Farmer’s Markets located in Chicago neighborhoods. Our API will support two basic user types:
Customers (Public)
Vendors
git clone https://github.com/pschmidtudig/farmers-market.git
npm install
npm install db-migrate -g
If you haven’t already (see pre-requisites), install postgres on your local machine and create a database called market.
db-migrate up
npm run start
query {
vendors {
nodes {
name
products {
nodes {
name
}
}
locations {
nodes {
name
}
}
}
}
}
mutation {
createProduct(input:{
product:{
name:"Salt Water Taffy"
}
}) {
product {
name
}
}
}
If you review the code base, you’ll notice that there is very little code or configuration required to get our project up and running. That’s because postgraphile is doing a ton of work for us behind the scenes. Let’s take a look at our server.js file:
//server.js
const express = require("express");
const { postgraphile } = require("postgraphile");
require("dotenv").config();
const pgManyToManyPlugin = require("@graphile-contrib/pg-many-to-many")
const pgSimplifyInflectorPlugin = require("@graphile-contrib/pg-simplify-inflector")
const dbSettings = require('../data/database.json')
const app = express();
app.use(
postgraphile(
dbSettings.dev,
"public",
{
appendPlugins: [
pgManyToManyPlugin, // Generates queries to support many to many relationship querying
pgSimplifyInflectorPlugin, // Simplifies generated query names, for example "allVendors" becomes "vendors"
],
watchPg: true, // Postgraphile updates automatically if you change your postgres schema
graphiql: true, // Spins up GraphiQL query browser when you start the server
showErrorStack: "json", // Includes error details in responses
extendedErrors: ["hint", "detail", "errcode"] // Includes additional error details in responses
}
)
);
app.listen(process.env.PORT || 3000)
In this file, we’re initializing an instance of Express, then configuring it to use postgraphile (postgraphile as a library: https://www.graphile.org/postgraphile/usage-library/). Below is a description of the postgraphile arguments:
A key component of the postgraphileOptions object is the appendPlugins property. Later in this article, we’ll add a new custom plugin to this array to further customize the behavior of our GraphQL API.
Our goal is to protect our GraphQL API by requiring users who want to edit data to be authenticated with Auth0 and authorized with the proper permissions. To do this, we first need to configure Auth0 for use in our application.
The following steps describe how to configure your Auth0 environment to support the GraphQL API backend. If you haven’t already, you can setup a free Auth0 account to use for this tutorial.
Create a Tenant
Create an API
Next, we need to create an API application in Auth0 that we’ll interface with from our postgraphile project.
Configure a Test User
Next, we need to create a test (vendor) user with the proper privileges for editing data in our GraphQL API.
Now that we have our Auth0 environment configured, we can update our GraphQL API project to support authorized users.
Add an .env file
Create a .env file in the root folder of your project to store the Auth0 configuration. Add two entries named AUTH0_AUDIENCE and AUTH0_DOMAIN and configure them as follows:
AUTH0_AUDIENCE=farmers-market-api
AUTH0_DOMAIN=https://{your_auth0_domain}.us.auth0.com/
Add Dependencies
Run the following commands from your root folder to install the packages that we’ll use for validating JWT tokens from Auth0:
npm install --save jwks-rsa
npm install --save express-jwt
Update server.js to process jwt tokens
Add references to both of the installed packages at the top of your server.js file:
// server.js
const jwksRsa = require("jwks-rsa");
const jwt = require("express-jwt");
{...}
Add the following code between where you initialize express (const app = express();) and the postgraphile declaration (app.use( postgraphile( … )).
// server.js
{...}
const jwksUri = process.env.AUTH0_DOMAIN + ".well-known/jwks.json"
const checkJwt = jwt.expressjwt({ secret: jwksRsa.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: jwksUri }), credentialsRequired: false, //When false, the API will still process JWT tokens but will not require a user to be authenticated audience: process.env.AUTH0_AUDIENCE, issuer: process.env.AUTH0_DOMAIN, algorithms: ["RS256"],
});
app.use("/graphql", checkJwt); //Add token-checking process to /graphql endpoint
{...}
With these changes, Postgraphile will check for a Bearer token in the Authorization header before continuing to process a request. Since we allow all data in our GraphQL API to be read by the public, we set the credentialsRequired flag to false.
Update server.js to Retrieve User Permissions from Access Token
Once we confirm that a user’s authorization token is valid, we need to make sure that the user’s token information is passed into the GraphQL context. This will allow us to examine a user’s claims and make decisions about what data they can see based on those claims.
To do this, we’ll add an additional option in our postgraphile configuration called
additionalGraphQLContextFromRequest.
Each time a request is executed against the API, this code will intercept the request object, check to see if there is an auth property on the request (added from our jwt validation above), and if so, assign the permissions from Auth0 to a user object. By doing so, we’re making user available as part of the Express server context.
// server.js{...}
app.use(
postgraphile(
process.env.DATABASE_URL,
"public",
{
watchPg: true,
{...}
extendedErrors: ["hint", "detail", "errcode"],
additionalGraphQLContextFromRequest: req => {
if(req.auth != undefined) {
return {
user: {
permissions: req.auth.permissions
}
}
}
}
}
)
);
Note: If you want to test your code at this point, jump down to Testing with an Authenticated/Authorized User for instructions on retrieving an access token from Auth0.
In order to make use of our newly available user permission data, we need to configure GraphQL Shield.
GraphQL Shield is made up of two main components:
Run the following command from your root folder to install the graphql-shield and @graphql-tools/wrap packages:
npm install --save graphql-shield
npm install --save @graphql-tools/wrap
Create a new folder in your src folder called auth. Add the following files:
The first thing that we need to do is create some rules around who can access the data in our API. We’ll first create a rule called isAuthenticated that simply checks that a user is calling the API with a valid token from Auth0.
The second rule, canEditData will perform an additional check to see if a user has the write:all permission.
// rules.jsconst { rule } = require('graphql-shield');
const isAuthenticated = rule({ cache: 'contextual' })(
async (parent, args, ctx, info) => {
return ctx.user !== undefined;
}
);
const canEditData = rule({ cache: 'contextual' })(
async (parent, args, ctx, info) => {
return ctx.user != undefined && ctx.user.permissions.includes("write:all");
}
);
module.exports = {
isAuthenticated, canEditData
}
Now that we have a set of rules, we need to add them to the appropriate GraphQL endpoints or objects that we want to protect.
First, we’ll update the Vendor object so that only authenticated users can get a vendor’s contact information (email). Next, we’ll lock down the entire Mutation object, so only authenticated users with edit permissions can execute mutations on our API.
For illustration purposes, I also added an option called fallbackRule to our permissions. We’ll set it to true here, which is the default value, but if you set this value to false, any object/field in the API without an explicit allow rule will have a deny rule by default.
// permissions.jsconst rules = require('./rules')
const { shield, allow } = require('graphql-shield')
const permissions = shield({
Vendor: {
email: rules.isAuthenticated
},
Mutation: rules.canEditData
},
{ fallbackRule: allow },
);
module.exports = permissions;
Because we’re using postgraphile to setup our API, we need to perform an additional step to apply the graphql-shield middleware to our API.
Create a new subfolder in your src directory called plugins. In it, add a new file called graphQLShieldPlugin.js. Add the following code:
// graphQLShieldPlugin.jsconst { makeProcessSchemaPlugin } = require("graphile-utils");
const permissions = require('../auth/permissions')
const { wrapSchema } = require('@graphql-tools/wrap')
const { applyMiddleware } = require('graphql-middleware')
module.exports = makeProcessSchemaPlugin(schema => {
const wrappedSchema = wrapSchema({
schema: schema,
transforms: []
});
return applyMiddleware(wrappedSchema, permissions)
});
The key line in the code above is return applyMiddleware(wrappedSchema, permissions.permissions). This takes our generated graphQL schema as an input and applies our graphql shield permissions.
I’ve added the wrapSchema step as a workaround for an error that occurs when using postgraphile with graphql-shield, described here: https://github.com/maticzav/graphql-middleware/issues/369
For more information on schema wrapping with @graphql-tools/wrap, see https://www.graphql-tools.com/docs/schema-wrapping
Finally, we need to update our server.js code to include our new plugin.
Add a reference to the plugin file at the top of server.js:
const graphQLShieldPlugin = require(‘./plugins/graphQLShieldPlugin’);
Then add graphQLShieldPlugin to appendPlugins on the list of postgraphile options:
app.use( postgraphile(
dbSettings.dev,
"public",
{
appendPlugins: [
pgManyToManyPlugin,
pgSimplifyInflectorPlugin,
graphQLShieldPlugin,
],
Restart the server with the latest updates (npm run start). With the changes we’ve applied to our project, we should still be able to execute the following query in GraphiQL (http://localhost:3000/graphiql):
Request:
query { vendors {
nodes {
name
}
}
}
However, if you add email to the request, you should get an Authorization error, since we require a user to be authenticated to query that field:
vendors {
nodes {
name
email
}
}
}
You should see a similar error when attempting to execute a mutation.
In order to verify that our GraphQL Shield rules are operating as expected, we need to add a valid Authorization header to our GraphiQL call. For that, we need to get an authorization token from Auth0.
The easiest way to retrieve an authorization token from Auth0 is by using their Auth0 Authentication API Debugger extension through the Auth0 dashboard. However, to use the debugger, we first we need to configure an Application to represent the client that we’re logging in from.
Finally, all we need to do to access our GraphQL mutations is add the valid user access token to our request headers.
Navigate to GraphiQL and open up the Request Headers tab in the window below the query editor. Add an Authorization header with a value of “Bearer {access_token}”:
If the token you provided is valid, you should now be able to execute the following query:
query { vendors {
nodes {
name
email
}
}
}
Since our user has the write:all permission, you should also be able to execute mutations:
mutation { createVendor(
input: {
vendor: {
name: "Lucy's Coffee and Ice Cream"
description: "Espresso and hand-churned ice cream"
}
}
) {
vendor {
name
description
}
}
}
As an additional test, try creating an additional user in Auth0 without adding any permissions. The new user should be able to query the email property on a Vendor, but should get an Unauthorized error if they try to execute a mutation!
If you run into any issues or want to compare your code against the expected state, take a look at the _farmers-market-with-auth folder in the repo, which contains the completed code from this tutorial.
You should now have a basic GraphQL API setup using postgraphile that can be secured with Auth0 and GraphQL Shield rules. Now that you have the basic configuration in place, here are some additional things to try:
One thing you may notice is that, in the current state of our project, any user with write privileges can edit any data in our API. This is obviously not ideal, as we mentioned before that vendors should only have access to edit their own data. Look out for a future blog where we’ll address this problem using row level security in Postgres!
Page is a Principal Consultant on UDig's Software team.