Introduction
GraphQL APIs provide clients with the flexibility to request precisely the data they need. However, security is vital to prevent unauthorized data access in GraphQL, especially for sensitive information.
A common way to secure GraphQL is by implementing JSON Web Token (JWT) based authentication. JWTs allow stateless user authentication by encoding user credentials and permissions in compact self-contained tokens.
This comprehensive guide will teach you how to add JWT authentication to lock down your GraphQL APIs built with Express.js.
Overview of JSON Web Token (JWT) Based Auth Flow
Here is an overview of how JWT auth works with GraphQL:
This allows the client to make authenticated API calls by passing the JWT without resending credentials. The token encapsulates the user identity and privileges.
Next, let’s implement this end-to-end with Express serving GraphQL APIs.
1. Set Up Express App and GraphQL Server to serving GraphQL APIs
We’ll use the Express web framework along with Apollo Server to implement our GraphQL backend.
Initialize a Node.js project and install required packages:
# Install Express, Apollo Server, GraphQL
npm install express apollo-server-express graphql
In app.js
, set up an Express app:
// app.js
const express = require('express');
const app = express();
app.listen({ port: 4000 }, () => {
console.log('Server ready at http://localhost:4000');
});
Next, create an Apollo Server instance and apply it as Express middleware:
const { ApolloServer } = require('apollo-server-express');
const server = new ApolloServer({});
server.applyMiddleware({ app, path: '/graphql' });
This mounts the GraphQL endpoint at /graphql
.
We can now send GraphQL queries to http://localhost:4000/graphql
. But first, we need to define the schema.
2. Define GraphQL Schema and Resolvers
Let’s define a simple schema in schema.js
with a single posts
query:
const { gql } = require('apollo-server-express');
const typeDefs = gql`
type Post {
id: ID!
title: String!
content: String!
}
type Query {
posts: [Post!]!
}
`;
module.exports = typeDefs;
This defines a Post
type with some fields, and a posts
query that returns a list of posts.
Next, we need the corresponding resolvers in resolvers.js
:
const resolvers = {
Query: {
posts: () => {
return postsList;
},
},
};
module.exports = resolvers;
The resolver simply returns a mock postsList
for now.
We can pass these to Apollo Server:
const server = new ApolloServer({
typeDefs,
resolvers,
});
This sets up a basic GraphQL server.
3. Hash User Passwords
Before implementing JWT auth, we need a mock database of users.
Let’s define an array of users with hashed passwords in users.js
:
const bcrypt = require('bcrypt');
const users = [
{
id: 1,
username: 'john',
password: bcrypt.hashSync('password123', 10)
},
{
id: 2,
username: 'jane',
password: bcrypt.hashSync('password456', 10)
},
];
module.exports = users;
We hash the plaintext passwords using the bcrypt
library before storing so the database only contains secured passwords.
This will be our mock user database for authentication.
4. Implement User Login and JWT Generation
When the user tries to login with credentials, we need to:
- Validate credentials against the database
- On success, generate a signed JWT encoding the user ID
- Return the JWT to be stored by the client
Add the following resolver in resolvers.js
to handle login:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const users = require('./users'); // our mock db
const resolvers = {
Mutation: {
login: async (parent, args) => {
const user = users.find(
u => u.username === args.username
);
if (!user) {
throw new Error('Invalid username');
}
const valid = await bcrypt.compare(
args.password,
user.password
);
if (!valid) {
throw new Error('Invalid password');
}
// Return JSON Web Token
return jwt.sign(
{ userID: user.id },
process.env.SECRET_KEY,
{ expiresIn: '1d' }
);
}
}
}
The login
mutation accepts a username and password. We first fetch the user from our mock database and compare the hashed password.
On success, we generate a JWT payload containing the userID
, sign it using a secret key, and return the token. The client can store this for subsequent authorized requests.
5. Pass JWT in Authorization Header
Let’s create a simple React app as our client.
Install Apollo Client:
npm install @apollo/client graphql
In App.js
, initialize the client with the GraphQL API url:
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
});
We’ll use the useMutation
hook to call the login mutation:
const LOGIN = gql`
mutation Login($username: String!, $password: String!) {
login(username: $username, password: $password)
}
`;
function App() {
const [login] = useMutation(LOGIN);
const handleLogin = async () => {
const { data } = await login({
variables: {
username: 'john',
password: 'password123'
}
});
console.log(data.login); // print JWT
};
}
This calls the login
mutation, passing the sample credentials. On success, it prints the returned JWT.
We need to pass this JWT in subsequent requests. The Apollo Client lets us define a request pipeline to inject the token header:
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
request: async (operation) => {
const token = window.sessionStorage.getItem('token');
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
}
});
This intercepts each request to get the stored token from sessionStorage
and inject it in the Authorization
header as a bearer token before sending the request.
Now the API will receive the header Authorization: Bearer <jwt>
allowing it to validate the token.
6. Verify JWT on GraphQL Requests
To validate the JWT, let’s create some middleware in auth.js
:
const jwt = require('jsonwebtoken');
const verifyJWT = (req) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.SECRET_KEY);
return decoded;
} catch {
return null;
}
};
module.exports = verifyJWT;
This middleware tries to verify the JWT using the secret key. On success, the decoded token is returned. On error, null
is returned.
We can apply it in Apollo Server by modifying server.js
:
const verifyJWT = require('./auth');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const user = verifyJWT(req);
return { user };
}
});
The middleware runs on each request, decodes the token if present, and sets the user
context value accordingly.
7. Authorize Resolver Execution Based on User Context
Now we can authorize our resolvers by checking the user
context:
const resolvers = {
Query: {
posts: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('Unauthorized');
}
// return posts
}
}
}
If request is unauthenticated, context.user
will be null
and we throw an error.
For authorized users, we can access the decoded token contents via context.user
inside resolvers to customize data or permissions.
This completes JWT authentication! Only users who provide a valid signed token can access the GraphQL API.
Conclusion
Some key points about implementing JWT auth with GraphQL:
- User provides credentials, server returns signed JWT
- Client stores and sends JWT in authorization header
- Middleware decodes JWT into user context
- Resolvers use context to authorize and customize data
This stateless approach to auth using compact self-contained tokens works well for GraphQL’s flexible query model.
Additional enhancements like combining JWT with custom directives, role-based auth, or OAuth integration can build further authorization logic.
Secure your GraphQL backend with authenticated clients by implementing JWT validation on the server. This will prevent unauthorized data access especially when exposing powerful GraphQL APIs to clients.
Frequently Asked Questions
Here are some common questions about securing GraphQL APIs with JWT auth:
How does JWT work without sessions?
JWT encodes user identity and privileges in the self-contained signed token itself, eliminating the need for server-side sessions.
Where should the client store the JWT?
Local storage, cookies or state management tools like React Context/Redux are good options.
How long should the token expiry be set?
Depends on the app, but tokens usually expire anywhere between 15 minutes to a few days. Shorter expiry enhances security.
Should refresh tokens also be used?
Yes, using a long-lived refresh token to obtain fresh short-lived access tokens enhances security.
How can roles rules be encoded in the token?
Custom claims like "roles": ["editor", "user"]
can be added to the JWT payload along with the standard claims.