- 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
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.
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.
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.
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}`); | |
}); |
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; | |
} | |
} | |
}; |
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" | |
} | |
]; |
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); | |
} | |
} | |
}; |
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!
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.
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.'); | |
} | |
} | |
} |
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}`); | |
}); |
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.
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(); |
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.
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:
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.'); | |
} | |
} | |
} |
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(); |
{ | |
"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" | |
} | |
} |
module.exports = [ | |
{ | |
authorId: 1, | |
title: "Post 1" | |
}, | |
{ | |
authorId: 1, | |
title: "Post 2" | |
}, | |
{ | |
authorId: 2, | |
title: "Post 3" | |
}, | |
{ | |
authorId: 2, | |
title: "Post 4" | |
} | |
]; |
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 }; | |
} | |
} | |
}; |
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}`); | |
}); |
Interested in more software development tips & insights? Visit the development section on our blog!
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