loading...

How to Auth: Securing Your GraphQL API with Confidence

mandiwise profile image Mandi Wise ・15 min read

The following post is based on the code I demoed during my GraphQL Summit 2020 talk. You can find the recording of that talk here and be sure to check out the entire playlist of awesome talks here.


When building a GraphQL API, we often need to limit access to queries and mutations depending on who is requesting the data. The GraphQL spec doesn't provide any specific guidelines for how to manage "auth" with GraphQL, so it's up to us to choose our own adventure!

That said, it's a good idea to draw from battle-tested practices that have emerged over the years when it comes to layering authentication (who a user is) and authorization (what a user can do) onto a GraphQL API. In this post, I'll explore how we can use these best practices so we can lock down a GraphQL API with confidence.

Starting Point

Before we jump into the code, it's important that we clarify a few assumptions we're going to make as we build out our API.

First, we're not going to lock down our entire GraphQL API endpoint. We will typically want to authorize user access to our API on a per-query or per-mutation basis. We may even want to manage access more granularly on a per-field basis. As a result, we'll need a more nuanced approach than protecting the entire API at the endpoint level.

Our next assumption is that we're going to use token-based authentication. Specifically, we'll use a JSON Web Token (JWT), but you could use a similar approach with other kinds of tokens too.

Finally, we're going to use Express with Apollo Server because it will simplify the JWT handling and verification process with some ready-to-go middleware, but it's not explicitly necessary to use Express or any specific kind of Node middleware to do this (though highly recommended!).

Installfest and Set-up

We'll begin by creating a directory for our project files:

mkdir basic-apollo-auth-demo && cd basic-apollo-auth-demo

Inside the new directory, we'll run npm init --yes to create a package.json file pre-populated with default values:

npm init --yes

Next, we'll install all of the dependencies we need for this project:

npm i apollo-server-express@2.15.1 esm@3.2.25 express@4.17.1 express-jwt@6.0.0 graphql-middleware@4.0.2 graphql-shield@7.3.2 jsonwebtoken@8.5.1 nodemon@2.0.4

Here's a quick summary of what we'll use each package for:

  • apollo-server-express: To facilitate integrating Node.js middleware with our server, we'll use the Apollo/Express integration.
  • esm: This package is a "babel-less, bundle-less ECMAScript module loader" that will allow us to use import and export in Node.js without any hassle.
  • express: Again, we'll use Express to add some middleware to our server.
  • express-jwt: This Express middleware will conveniently verify and decode an incoming JWT and add it to the Express req object for us.
  • graphql: Apollo requires this library as a peer dependency.
  • graphql-middleware: This package will allow us to wrap our schema so that we can execute code (i.e. permission checks!) before our resolver functions run.
  • graphql-shield: GraphQL Shield will allow us to add an authorization layer to our GraphQL API as middleware.
  • jsonwebtoken: We'll use this package to create and sign a JWT when a user logs in.
  • nodemon: Nodemon will automatically reload our application when files change in the project directory.

We'll also add a directory to organize our project and create a few files in it too:

mkdir src && touch src/index.js src/typeDefs.js src/resolvers.js src/data.js

Let's start with src/data.js. Rather than using a database we'll work with mocked data in our resolvers, so we'll need to add that data to this file:

export const users = [
  {
    id: "12345",
    name: "Gene Kranz",
    email: "gene@nasa.gov",
    password: "password123!",
    roles: ["director"],
    permissions: ["read:any_user", "read:own_user"]
  },
  {
    id: "67890",
    name: "Neil Armstrong",
    email: "neil@nasa.gov",
    password: "password890!",
    roles: ["astronaut"],
    permissions: ["read:own_user"]
  }
];

Next, we'll add an object type called User with a corresponding query to fetch a single user by their ID in src/typeDefs.js:

import { gql } from "apollo-server-express";

export default gql`
  type User {
    id: ID!
    name: String
  }

  type Query {
    user(id: ID!): User
  }
`;

We'll also need to add a resolver for the user query to src/resolvers.js:

import { users } from "./data";

export default {
  Query: {
    user(parent, { id }) {
      return users.find(user => user.id === id);
    }
  }
};

In our src/index.js file, we can now set up Apollo Server with Express. We set up our ApolloServer as usual, passing in the imported typeDefs and resolvers, and then we integrate Express with Apollo Server by calling the applyMiddleware method on the new ApolloServer instance and pass in the top-level Express app:

import { ApolloServer } from "apollo-server-express";
import express from "express";

import resolvers from "./resolvers";
import typeDefs from "./typeDefs";

const port = 4000;
const app = express();

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

server.applyMiddleware({ app });

app.listen({ port }, () => {
  console.log(`Server ready at http://localhost:${port}${server.graphqlPath}`);
});

Lastly, we'll add a script to our package.json file that will allow us to start up our GraphQL API:

{
  // ...
  "scripts": {
    "server": "nodemon -r esm ./src/index.js"
  },
  // ...
}

Now we can run npm run server and we should be able to test our API in GraphQL Playground at http://localhost:4000/graphql. Try running a user query to get one of the users by their ID to make sure that it works before moving on to the next section.

Make Incoming JWT Available to Resolvers

As previously mentioned, we're going to use JWTs to help protect our API. Specifically, we will require a valid JWT to be sent in the Authorization header of every request. JWTs conform to an open standard that describes how information may be transmitted as a compact JSON object and they consist of three distinct parts:

  1. Header: Contains information about the token type and the algorithm used to sign the token (for example, HS256).
  2. Payload: Contains claims about a particular entity. These statements may have predefined meanings in the JWT specification (known as registered claims) or they can be defined by the JWT user (known as public or private claims).
  3. Signature: Helps to verify that no information was changed during the token’s transmission by hashing together the token header, its payload, and a secret.

A typical JWT will look something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL3NwYWNlYXBpLmNvbS9ncmFwaHFsIjp7InJvbGVzIjpbImFzdHJvbmF1dCJdLCJwZXJtaXNzaW9ucyI6WyJyZWFkOm93bl91c2VyIl19LCJpYXQiOjE1OTQyNTI2NjMsImV4cCI6MTU5NDMzOTA2Mywic3ViIjoiNjc4OTAifQ.Z1JPE53ca1JaxwDTlnofa3hwpS0PGdRLUMIrC7M3FCI

Even though the JWT above may look encrypted, it has only been base64url-encoded to make it as compact as possible. That means that all of the information inside can just as easily be decoded again. Similarly, the signature portion of the JWT only helps us ensure that data hasn't been changed during its transmission, so it’s important to not put any secret information inside the JWT header or payload in cleartext.

The header section of the above token would decode to:

{
  "alg": "HS256",
  "typ": "JWT"
}

And the payload section would decode as follows:

{
  "https://spaceapi.com/graphql": {
    "roles": ["astronaut"],
    "permissions": ["read:own_user"]
  },
  "iat": 1594252663,
  "exp": 1594339063,
  "sub": "67890"
}

In the token’s payload, the sub, iat, and exp claims represent registered claims. The sub claim (short for “subject”) is a unique identifier for the object described by the token. The iat claim is the time at which the token was issued. The exp claim is the time that the token expires. These claims are a part of the JWT specification.

The claim with the https://spaceapi.com/graphql key is a user-defined claim added to the JWT. Custom public claims included in a JWT must be listed in the IANA JSON Web Token Registry or be defined with a collision-resistant namespace such as a URI, as was done above.

You can experiment with encoding and decoding JWTs at https://jwt.io.

At this point, you may be wondering how we'd use a JWT during the authentication process and how we can use the data contained within to authorize a user to access various features of our API. At a high level, when a user logs in—with their username and password in our case—the server will verify their credentials against the data saved in the database and then create a signed JWT to send back to the client.

The user can then send this token back to the server with every subsequent request (until the JWT expires) so the server can verify the JWT and respond with the protected data if the JWT is valid. In the example that follows, we'll send the JWT to the server in the Authorization header of each request.

To simplify the JWT-handing process, we'll use the express-jwt package we previously installed to add middleware to Express that will intercept an incoming JWT, verify and decode it, and then add the decoded token to the req object as a user property.

Let's add the middleware in src/index.js now:

import { ApolloServer } from "apollo-server-express";
import express from "express";
import expressJwt from "express-jwt"; // NEW!

// ...

app.use(
  expressJwt({
    secret: "SUPER_SECRET",
    algorithms: ["HS256"],
    credentialsRequired: false
  })
); // NEW!

// ...

Above, we've called the expressJwt function and pass in a secret string to sign the JWT. For demonstration purposes only, the secret has been added directly to this file but you would likely want to keep track of this value in an environment variable instead.

We also specify the signing algorithm to be HS256. HS256 is a symmetric signing algorithm so we'll need to use the same secret when verifying it and when we later create a JWT when the user signs in.

Lastly, we set the credentialsRequired option to false so Express won’t throw an error if a JWT hasn’t been included, which would be the case when a user initially logs in or when GraphQL Playground polls for schema updates.

And if you're wondering what kind of middleware magic happens under the hood here, express-jwt will get the token from the Authorization header of an incoming request, decode it, and add it to the req object as the user property.

Next, we can use the Apollo Server's context option to access the decoded token from the req object and pass this data down the graph to our resolvers. It’s a common practice to add decoded tokens to Apollo Server’s context because this object is conveniently available in every resolver and it’s recreated with every request so we won’t have to worry about tokens going stale.

In src/index.js, we'll check for the user object in the request and add it to the Apollo Server context if it exists, otherwise we just set the user to null because we don't want to error out here if a token isn't available:

// ...

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const user = req.user || null;
    return { user };
  } // UPDATED!
});

// ...

With this code in place, if a JWT accompanies any request to our API, then we'll be able to access the decoded object from the context parameter of any resolver function.

Add a Login Mutation

Now that we can handle an incoming token, we need a way to create one in the first place when a user wants to log in. And this leads us to a very big question—should authentication be handled within the GraphQL server, or is this out of band?

Some people say we should leave authentication entirely out of the schema. In other words, we shouldn't have login or logout mutations. That would mean we just have the decoded token data available in the resolvers' context parameter and leave it at that.

I would say, in practice, there's a very high likelihood that you would want to use some kind of dedicated auth service (and perhaps even use something like Auth0) to manage your app's authentication needs. However, to keep things manageable for the scope of this tutorial, we'll implement a login mutation so we can get a sense of how JWT would be created.

To do this, we'll need to update src/typeDef.js:

import { gql } from "apollo-server-express";

export default gql`
  # ...

  type Mutation {
    login(email: String!, password: String!): String
  } # NEW!
`;

And over in src/resolvers.js, we'll add a login resolver that finds the user in our database whose email and password match the incoming arguments, and then we'll use the jsonwebtoken package to create and sign a JWT for them:

import jwt from "jsonwebtoken";

import { users } from "./data";

export default {
  // ...
  Mutation: {
    login(parent, { email, password }) {
      const { id, permissions, roles } = users.find(
        user => user.email === email && user.password === password
      );
      return jwt.sign(
        { "https://spaceapi.com/graphql": { roles, permissions } },
        "SUPER_SECRET",
        { algorithm: "HS256", subject: id, expiresIn: "1d" }
      );
    }
  } // NEW!
};

The first argument we pass into the sign method above is an object containing the JWT information we want to add to the payload of the token. And because we're adding some custom info to this token, we namespace it using the URL of the GraphQL API as property with the user's permissions and roles as a value.

As a second option, we pass in the same secret that we used to verify the token before. And as a third option, we can pass in additional options such as the unique subject value (which is the user's ID), a token expiration time, and the signing algorithm we want to use.

Add a Viewer Query

We have one final step to complete before we can test out our updated code in GraphQL Playground. We're going to add a viewer query that will return the authenticated user based on the token included in the Authorization header of the request.

We'll updated our code in src/typeDefs.js:

import { gql } from "apollo-server-express";

export default gql`
  # ...

  type Query {
    user(id: ID!): User
    viewer: User! # NEW!
  }

  # ...
`;

As a sidebar here, it's a good practice to expose a viewer query that acts as the entry point for what an authenticated user can do with an API. If we were to fully realize that in our API, we could add a Viewer object type to use as the return type for the viewer query and expose fields on that type that allow an authenticated user to query relevant data. I encourage you to take a look at the GitHub GraphQL API for a working implementation of this.

We'll also need to add the corresponding resolver in src/resolvers.js:

import jwt from "jsonwebtoken";

import { users } from "./data";

export default {
  Query: {
    // ...
    viewer(parent, args, { user }) {
      return users.find(({ id }) => id === user.sub);
    } // NEW!
  },
  // ...
};

In the code above, we get the currently authenticated user's information by using their ID value, which is available in the sub claim of the decoded token in the context object parameter.

We're now ready to try out our API again in GraphQL playground. Let's try running a login mutation first:

mutation {
  login(email: "neil@nasa.gov", password: "password890!")
}

The login mutation will return a JWT like this:

{
  "data": {
    "login": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL3NwYWNlYXBpLmNvbS9ncmFwaHFsIjp7InJvbGVzIjpbImFzdHJvbmF1dCJdLCJwZXJtaXNzaW9ucyI6WyJyZWFkOm93bl91c2VyIl19LCJpYXQiOjE1OTU3MDA2ODUsImV4cCI6MTU5NTc4NzA4NSwic3ViIjoiNjc4OTAifQ.l4Afg9-suWBROzN7xU1qkZENgMWcy1msoekm8roSqsI"
  }
}

We can then copy that JWT and add it to the "HTTP Headers" panel of GraphQL Playground in this format:

{
  "Authorization": "Bearer "
}

Now we can try running the viewer query with this header added:

query {
  viewer {
    name
  }
}

And we'll see that we get back information about the authenticated user, as expected:

{
  "data": {
    "viewer": {
      "id": "67890",
      "name": "Neil Armstrong"
    }
  }
}

Add Authorization by Checking Permissions

Before we can finish building our GraphQL API, we need to understand a few things about authorization. While we now have a way to identify users based on tokens in place, we still don’t have any mechanism to limit API access to authenticated users This is where authorization comes in!

The most basic level of authorization is letting users run queries based on whether they are authenticated, and we're going to do this, but we'll also add finer-grained authorization to our queries based on the permissions in the logged-in user's JWT.

When adding authorization to GraphQL API, we have a few different options available. We could directly check the authenticated user’s ID and permissions inside of each resolver, but this wouldn't be very DRY, so let's just count that one as off the table.

Instead, one popular option for adding authorization involves adding custom schema directives to control access to various types and fields. Alternatively, we could use a package like GraphQL Auth to wrap our resolver functions explicitly with permission checks. Similarly, we could use a package like GraphQL Shield to completely abstract the authorization rules into a middleware layer.

For our API, we'll choose GraphQL Shield. First, we'll need to add a permissions.js file to our project:

touch src/permissions.js

Inside of src/permissions.js, we'll first create a little helper function that we'll use to check if a decoded user token has a permission applied:

function checkPermission(user, permission) {
  if (user && user["https://spaceapi.com/graphql"]) {
    return user["https://spaceapi.com/graphql"].permissions.includes(
      permission
    );
  }
  return false;
}

Next, we'll import a few things into this file from GraphQL Shield that will help us apply authorization rules to our schema. First, we'll focus on the rule function, which has all of the same parameters as a typical resolver function, including the context.

We use the rule function to, not surprisingly, create an authorization rule. The first one we create will check if a user is authenticated by verifying that the decoded JWT is present in the context:

import { and, or, rule, shield } from "graphql-shield"; // NEW!

function checkPermission(user, permission) {
  if (user && user["https://spaceapi.com/graphql"]) {
    return user["https://spaceapi.com/graphql"].permissions.includes(
      permission
    );
  }
  return false;
}

const isAuthenticated = rule()((parent, args, { user }) => {
  return user !== null;
}); // NEW!

Note that if we return false from any rule, then authorization will be denied.

Now we can add some more complex rules to src/permissions.js that check what permissions have been assigned to a user:

// ...

const isAuthenticated = rule()((parent, args, { user }) => {
  return user !== null;
});

const canReadAnyUser = rule()((parent, args, { user }) => {
  return checkPermission(user, "read:any_user");
});

const canReadOwnUser = rule()((parent, args, { user }) => {
  return checkPermission(user, "read:own_user");
});

const isReadingOwnUser = rule()((parent, { id }, { user }) => {
  return user && user.sub === id;
});

The canReadAnyUser and canReadOwnUser rules each check for the corresponding permissions in the JWT and return false if they don't exist, and the isReadingOwnUser rule verifies that the ID of the user requested in the query matches the ID of the authenticated user.

A final step in src/permissions.js, we'll call the shield function and pass it an object whose shape mirrors our resolvers. Inside of this object, we'll use our newly created rules to describe how to check authorization for each query:

// ...

export default shield({
  Query: {
    user: or(and(canReadOwnUser, isReadingOwnUser), canReadAnyUser),
    viewer: isAuthenticated
  }
});

For the viewer query, we only require that a user is authenticated to run the query. For the user query, we employ the logical and and or functions provided by GraphQL Shield to check a more complex configuration of rules. For this case, we allow users to query for a user if they are requesting their user and have the read:own_user permission assigned to them. Alternatively, they can view any user if they have the read:any_user permission assigned.

Add Permissions as GraphQL Middleware

We're nearly done, but we have to make some updates to src/index.js to add the permissions as a middleware to the GraphQL API. We can do this using the GraphQL Middleware package and importing our permissions into this file as well:

import { ApolloServer, makeExecutableSchema } from "apollo-server-express"; // UPDATED!
import { applyMiddleware } from "graphql-middleware"; // NEW!
import express from "express";
import expressJwt from "express-jwt";

import permissions from "./permissions"; // NEW!
import resolvers from "./resolvers";
import typeDefs from "./typeDefs";

// ...

We'll also need to update our ApolloServer config to accept a schema with the middleware applied instead of directly taking the typeDefs and resolvers as options:

// ...

const server = new ApolloServer({
  schema: applyMiddleware(
    makeExecutableSchema({ typeDefs, resolvers }),
    permissions
  ), // UPDATED!
  context: ({ req }) => {
    const user = req.user || null;
    return { user };
  }
});

// ...

Our secured API is now ready to go! Let's head back over to GraphQL Playground to try it out. First, let's run the user query using the same Authorization header as before (which we obtained for the non-director user), but we'll try to retrieve information about the other user instead:

query {
  user(id: "12345") {
    name
  }
}

We'll see that we get back a "Not Authorised!" message instead of the user's data. However, we can rerun the query using the authenticated user's ID and we'll see that we get back a successful response:

query {
  user(id: "67890") {
    name
  }
}

You can try logging in and obtaining a token for the user with the director role as well now. If you use that token in the HTTP Headers panel when making a user query, then you'll be able to query for either user because you'll have the read:any_user permission available.

Summary

In this post, we went on a whirlwind tour of how authentication and authorization can be handled with a GraphQL API using Express and Apollo Server.

Specifically, we saw how we can handle incoming JWTs in an Authorization header and pass that decoded data down the graph to resolvers. We also saw how a viewer query can act as an entry point for authenticated users to the API and how we can keep authorization checks out of resolvers functions by abstracting them into a middleware layer.

You can also find the complete code for this tutorial on GitHub.

If you enjoyed this post, I've also written a blog post about how to handle authentication and authorization with Apollo Federation on the official Apollo blog, as well as a post on using passwordless authentication with GraphQL, and you can read more about building full-stack JavaScript applications in my book Advanced GraphQL with Apollo & React.

Posted on by:

mandiwise profile

Mandi Wise

@mandiwise

JavaScript Developer 👩‍💻 Machine Learning Enthusiast 🤖 Author of Advanced GraphQL with Apollo & React 🚀

Discussion

markdown guide