Authorization and Authentication in GraphQL

Linden Melvin ·

Introduction

GraphQL is growing in popularity because it allows applications to request only the data they need using a strongly-typed, self-documenting query structure that enables an API to deliver data that can evolve over time.

Unlike traditional REST APIs, GraphQL exposes a single endpoint to query and mutate data. Upon learning this, one of the first questions that comes up for many developers is: “How do I implement authorization and authentication in GraphQL?”

Authorization and authentication in GraphQL can be perplexing if you are a developer coming from a REST API background. GraphQL is a surprisingly thin API layer. The spec is relatively short and is completely un-opinionated about how authorization and authentication are implemented, leaving the implementation details up to the developer.

Authorization patterns in GraphQL are quite different than in a REST API. GraphQL is not opinionated about how authorization is implemented. To quote directly from graphql.org, “Delegate authorization logic to the business logic layer.” It is up to the developer to handle authorization when using GraphQL.

GraphQL is also un-opinionated about how authentication is implemented. Authentication patterns in GraphQL, however, are very similar to patterns used in REST APIs: a user provides login credentials, an authentication token is generated and provided by the client in each subsequent request.

The implementation details for authorization and authentication in GraphQL can be a little tricky at first. With the help of a simple example GraphQL implementation, we can shed some light on how to approach these very important pieces of your API design.

Disclaimer

All examples in this post will be in JavaScript. We will be using Apollo to get things up and running. All of these concepts are applicable for a custom implementation of GraphQL, but Apollo takes boilerplate configuration out of the equation.

Authorization

Resolvers & Contexts

Resolvers are the building blocks of GraphQL used to connect the schema with the data. These functions define how to fetch data for a query and update data for a mutation. Since resolvers are simple functions that return data, they do not care about the structure of the underlying datastore.

Using Apollo, each resolver is a function that receives four parameters: root (or parent), args, context, and info. For the purposes of this post, we are interested in the context argument. It is the key to handling authorization in our Apollo server. context is passed to each resolver and contains data that each resolver can access. The importance of context is that it is initialized at the request level, meaning we can use it to store metadata about the user making the request. This is our entry point for implementing authorization.

Walkthrough

Let’s build a basic Apollo server:


const { ApolloServer, gql } = require('apollo-server');
const resolvers = require("./resolvers");
const typeDefs = gql`
type Post {
title: String!
authorId: String!
}
type Query {
posts: [Post!]!
}
`;
const server = new ApolloServer({
typeDefs,
resolvers
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

view raw

base.js

hosted with ❤ by GitHub

Our inline typeDefs defines our schema: there are Post objects that have a title and an authoId, and we have exposed one root query called posts which will allow us to fetch a list of Post objects.

The resolvers (in resolvers.js) are responsible for what it means to “fetch a list of Post objects.”:


const posts = require("./posts");
module.exports = {
Query: {
posts: () => {
return posts;
}
}
};

view raw

resolvers.js

hosted with ❤ by GitHub

The posts resolver simply returns the contents of posts.js. As mentioned before, GraphQL doesn’t care where the data comes from. That means, for the purposes of this simple example, our data can simply be an in-memory array. For example:


module.exports = [
{
authorId: 1,
title: "Post 1"
},
{
authorId: 1,
title: "Post 2"
},
{
authorId: 2,
title: "Post 3"
},
{
authorId: 2,
title: "Post 4"
}
];

view raw

posts.js

hosted with ❤ by GitHub

posts is an in-memory array of four Post objects. As you can see from the authorId on the post objects, we have posts from two different authors. Ideally, when a user makes a request for posts, we should only return the posts belonging to that user.

Now that we have the scaffolding for our Apollo server, let’s construct a context object:


const { ApolloServer, gql } = require('apollo-server');
const resolvers = require("./resolvers");
const typeDefs = gql`
type Post {
title: String!
authorId: String!
}
type Query {
posts: [Post!]!
}
`;
const context = () => {
return {
user: { id: 1 }
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

We are defining a context object and storing a user value in it. The user value we are defining here will be provided to each resolver through the context object. Let’s not worry about authenticating the user just yet. Instead, we will assume that the user has an id of 1 and that value will be stored in our context.

The resolver function for fetching posts is passed four arguments, including the context object. Using context, we are able to access the current user’s id and only return posts that belong to that user:


const posts = require("./posts");
module.exports = {
Query: {
posts: (_parent, _args, context, _info) => {
return posts.filter(post => post.authorId === context.user.id);
}
}
};

view raw

resolvers.js

hosted with ❤ by GitHub

This demonstrates how we can leverage the context object to have resolver-level visibility into who is trying to access the data.

Of course, we could expand this: instead of simply passing an id for the user, we could easily pass a more complex object containing roles or permissions. For example:


const posts = require("./posts");
module.exports = {
Query: {
posts: (_parent, _args, context, _info) => {
return posts.filter(post => {
const isAuthor = post.authorId === context.user.id;
const isAdmin = context.user.roles.includes("admin");
return isAuthor || isAdmin;
});
}
}
};

If we assume we have a value of admin as a possible role, we now have the ability to let a user see a post if they own it OR they are an admin!

Authentication

JWT

With an understanding of how to approach authorization in GraphQL, the next step is to figure out how we can authenticate a user so we can use a real value in the context object.

Similar to authorization, GraphQL is not opinionated about how you go about implementing authentication. It is up to the developer to define a system for taking user authentication credentials, verifying them, and giving the user access.

There are plenty of options when it comes to implementing authorization. For this example, we will implement a simple authentication system using JSON Web Tokens (JWT). JWT is used to securely send information between two parties as a JSON object, using a digital signature to verify the information has not been changed and can be trusted.

Walkthrough

Let’s start off by updating the resolvers and schemas from before to allow us to authenticate a user.


const posts = require("./posts");
const { authenticate } = require("./authService");
module.exports = {
Query: {
posts: (_parent, _args, context, _info) => {
return posts.filter(post => {
const isAuthor = post.authorId === context.user.id;
const isAdmin = context.user.roles.includes("admin");
return isAuthor || isAdmin;
});
}
},
Mutation: {
login: (_parent, args, _context, _info) => {
const { email, password } = args;
const token = authenticate(email, password);
return { token };
}
}
};

We have added a new Mutation called login that will allow us to handle login credentials provided by the client. To help handle the authentication logic, we have created an auth-service:


const jwt = require('jsonwebtoken');
module.exports = {
authenticate: (email, password) => {
let user;
// Implement the credential validation however you'd like.
if (email.length && password.length) {
user = {
id: 1,
roles: ["admin"]
}
}
if (!user) throw new Error("Invalid credentials.");
return jwt.sign(user, process.env.JWT_SECRET);
},
validateToken: token => {
try {
const { id, roles } = jwt.verify(token, process.env.JWT_SECRET);
return { id, roles };
} catch (e) {
throw new Error('Authentication token is invalid.');
}
}
}

view raw

auth-service.js

hosted with ❤ by GitHub

 

The auth-service uses JWT to generate a token that contains the id and roles of the authenticated user and that can be handed down to the client to stored in the Authorization header and be used in subsequent requests. For this example, the actual authentication logic is trivial, simply checking that the email and password values are not empty. This logic can be updated to fit your authentication needs.

Next, let’s look at the updated server file:


const { ApolloServer, gql } = require('apollo-server');
const { validateToken } = require("./authService");
const resolvers = require("./resolvers");
const typeDefs = gql`
type User {
id: ID!
email: String!
password: String!
}
type Post {
title: String!
authorId: ID!
}
type AuthResponse {
token: String!
}
type Query {
posts: [Post!]!
}
type Mutation {
login(email: String!, password: String!): AuthResponse!
}
`;
const context = ({ req }) => {
if (req.body.query.match("Login")) return {};
const authorizationHeader = req.headers.authorization || '';
const token = authorizationHeader.split(' ')[1];
if (!token) throw new Error("Authentication token is required.");
const user = validateToken(token);
return { user };
}
const server = new ApolloServer({
typeDefs,
resolvers,
context,
playground: false
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

view raw

server.js

hosted with ❤ by GitHub

As we saw in the resolvers, there is a new mutation available to us: login which will return the authenticated JWT.

The function responsible for constructing the context object has access to the request via the req parameter. This allows the context construction function to access the authorization header, grab the token, handle the case when the token does not exist, and then validate the token allowing us to get the user data encoded in it. Now that we have access to an authenticated user’s data, we can use this during authorization to make sure we are only returning data the user is allowed to see.

Using the API

We have created a query in our Apollo server that a client can use to fetch posts. First, though, the user must log in. Using the JavaScript HTTP client library axios, let’s take a look at how to dispatch a request to authenticate using our GraphQL API.


const axios = require("axios");
axios.defaults.baseURL = "http://localhost:4000/";
const login = async (email, password) => {
const axiosResponse = await axios.post('/', {
query: `mutation Login {
login(email: "${email}", password: "${password}") {
token
}
}`
});
const graphqlQueryData = axiosResponse.data;
const authenticationToken = graphqlQueryData.data.login.token;
axios.defaults.headers.common['Authorization'] = `Bearer ${authenticationToken}`;
}
const posts = async () => {
const axiosResponse = await axios.post('/', {
query: `query FetchPosts {
posts {
title
}
}`
});
const graphqlQueryData = axiosResponse.data;
const posts = graphqlQueryData.data.posts;
return posts;
}
const main = async () => {
await login("foo@example.com", "bar");
await posts();
}
main();

view raw

client-auth.js

hosted with ❤ by GitHub

The login method is used to dispatch a query to the Apollo API, providing the email and password from the user. The response contains a JWT token that we add to the axios client default headers for each subsequent request. When we then make a request for posts in the posts method, the response will contain all of the posts the user is authorized to see.

Conclusion

Authorization and authentication are fundamentally important pieces of API design. With many developers coming from a REST API background, making the leap to GraphQL can be confusing at first. This confusion stems from the fact that implementing authorization and authentication in GraphQL is left up to the developer. Using the example code provided in this post, you can create a fully functional GraphQL server using Apollo, complete with authorization and authentication.

The complete and functional example files are available here:

Setup

  • Clone all files into a directory
  • Run yarn
  • Run export JWT_SECRET="your_jwt_secret_key"
  • Run node server.js
  • Run node client-auth.js
  • See a console log lists of posts fetched from GraphQL

view raw

README.md

hosted with ❤ by GitHub


const jwt = require('jsonwebtoken');
module.exports = {
authenticate: (email, password) => {
let user;
// Implement the credential validation however you'd like.
if (email.length && password.length) {
user = {
id: 1,
roles: ["admin"]
}
}
if (!user) throw new Error("Invalid credentials.");
return jwt.sign(user, process.env.JWT_SECRET);
},
validateToken: token => {
try {
const { id, roles } = jwt.verify(token, process.env.JWT_SECRET);
return { id, roles };
} catch (e) {
throw new Error('Authentication token is invalid.');
}
}
}

view raw

auth-service.js

hosted with ❤ by GitHub


const axios = require("axios");
axios.defaults.baseURL = "http://localhost:4000/";
const login = async (email, password) => {
const axiosResponse = await axios.post('/', {
query: `mutation Login {
login(email: "${email}", password: "${password}") {
token
}
}`
});
const graphqlQueryData = axiosResponse.data;
const authenticationToken = graphqlQueryData.data.login.token;
axios.defaults.headers.common['Authorization'] = `Bearer ${authenticationToken}`;
}
const posts = async () => {
const axiosResponse = await axios.post('/', {
query: `query FetchPosts {
posts {
title
}
}`
});
const graphqlQueryData = axiosResponse.data;
const posts = graphqlQueryData.data.posts;
return posts;
}
const main = async () => {
await login("foo@example.com", "bar");
const postsForUser = await posts();
console.log("posts", postsForUser)
}
main();

view raw

client-auth.js

hosted with ❤ by GitHub


{
"name": "graphql-authorization-and-authentication",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"dependencies": {
"apollo-server": "^2.9.16",
"axios": "^0.19.2",
"jsonwebtoken": "^8.5.1"
}
}

view raw

package.json

hosted with ❤ by GitHub


module.exports = [
{
authorId: 1,
title: "Post 1"
},
{
authorId: 1,
title: "Post 2"
},
{
authorId: 2,
title: "Post 3"
},
{
authorId: 2,
title: "Post 4"
}
];

view raw

posts.js

hosted with ❤ by GitHub


const posts = require("./posts");
const { authenticate } = require("./auth-service");
module.exports = {
Query: {
posts: (_parent, _args, context, _info) => {
return posts.filter(post => {
const isAuthor = post.authorId === context.user.id;
const isAdmin = context.user.roles.includes("admin");
return isAuthor || isAdmin;
});
}
},
Mutation: {
login: (_parent, args, _context, _info) => {
const { email, password } = args;
const token = authenticate(email, password);
return { token };
}
}
};

view raw

resolvers.js

hosted with ❤ by GitHub


const { ApolloServer, gql } = require('apollo-server');
const { validateToken } = require("./auth-service");
const resolvers = require("./resolvers");
const typeDefs = gql`
type User {
id: ID!
email: String!
password: String!
}
type Post {
title: String!
authorId: ID!
}
type AuthResponse {
token: String!
}
type Query {
posts: [Post!]!
}
type Mutation {
login(email: String!, password: String!): AuthResponse!
}
`;
const context = ({ req }) => {
if (req.body.query.match("Login")) return {};
const authorizationHeader = req.headers.authorization || '';
const token = authorizationHeader.split(' ')[1];
if (!token) throw new Error("Authentication token is required.");
const user = validateToken(token);
return { user };
}
const server = new ApolloServer({
typeDefs,
resolvers,
context,
playground: false
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

view raw

server.js

hosted with ❤ by GitHub


Interested in more software development tips & insights? Visit the development section on our blog!

Join the Carbon Five Team collage of group photos with link to carbonfive.com/careers

We’re hiring! Looking for software engineers, product managers, and designers to join our teams in SF, LA, NYC, CHA.

Learn more and apply at www.carbonfive.com/careers