GraphQL Authentication using JSON Web Tokens

Introduction

In this tutorial we will discuss GraphQL authentication using JSON web tokens, which has been the prevalent way of handling authentication in the Javascript ecosystem for the last couple of years. We will also look at the concept of authorization by having a look at claims.

First let’s go over authentication and authorization in the context of web applications.

What is Authentication?

Authentication can be defined as a way of verifying your identity. One type of authentication could be a valid photo ID: the fact that you look like the person in the photo and the fact that the ID does not look to be a forgery authenticates that you are who you say you are. On web applications, we rely on usernames and passwords. When you enter your username and a password, that ideally is only known to you, you are authenticating that you are you, and you’re granted access.

What is Authorization?

We can think of authorization as the policies around who has access to a certain resource. For example, when you enter a concert as an audience member with a ticket that authenticates you, you are authorized to have access to the venue, to your seat, etc; but as a band member you have access to more areas, like backstage and the stage itself. Another example would be when you login into a social media platform you are authorized to delete any of your own posts but not the posts of other users. On the other hand, an administrator of that social media platform is authorized to delete any of the posts regardless of which user posted them.

Article overview

In this article we will first take a look at authentication by exploring JSON web tokens; from there we will take a look at the classic way of securing traditional web APIs, using middlewares, then we will take the knowledge from the previous two sections and apply them to GraphQL.

If you are only interested in authentication you don’t need to read the subsequent sections. With authentication under our belt we will then examine authorization: how it is used in traditional REST APIs and, from there, we will apply it to a GraphQL API.

Prerequisites

This article assumes a basic understanding of the following concepts, web servers inNode.js, middleware, and graphql-yoga, a GraphQL server.

Understanding and using JSON web tokens

Before going into how to apply JSON web tokens (JWT) in GraphQL, we need to understand what they are.

JWTs are built according to an open standard to be a self-contained way of securely sharing data as a JSON object. To secure its information JWTs using various form of encryption schemes. You can either securely encode your information (provided that you use the correct secret) or use private encryption. The differences between the two are out of the scope of this article, but you can find out more about the difference in this article

JSON web tokens structure

The structure of a JWT consists of three parts:

  • The Header: The header contains meta information such as the type of token, and the signature used for its content. The type would typically always be JWT. The signature will tell us which algorithm was used to sign the token.

  • The Payload: The payload usually contains the claims but it can encode any kind of information. Claims roughly contain the permissions that the bearer of the token has: for example, on a typical web app a token that has the claim “read-post”, can obtain a list of all of the posts on the web platform, or a user that has the claim “delete-post” can delete a post from the app. We’ll cover claims in more details in the authorization section.

  • The Signature: The signature contains the encoded signature that ensures the validity of the token and that the contents were not changed during transit.

Here is an example of a small token with all of its parts.

import * as jwt from 'jsonwebtoken'

const token = jwt.sign({ claims: 'read-post' }, 'secret', {
  algorithm: 'HS256',
})

console.log(token)
// header - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// payload - eyJkYXRhIjoiZm9vYmFyIiwiaWF0IjoxNTQ0NjQ0ODI0fQ.
// signature - IzNv-vRptcbU0S_qfqqKusEnwLzdUT_IsYjIcQl4KHk

How JSON web tokens secure an application

In this tutorial, for the sake of brevity, we will encode our token using HS256 with an insecure secret, but you can use a secure secret so that brute forcing your tokens encoded using HS256 is not feasible.

The jwt sign function has the following signature: jwt.sign(payload, secretOrPrivateKey, [options, callback]).

In the last example we encrypted the JSON { claims: "read-post" }, using the insecure secret secret.

JWT Flow

In a real application the JWT flow is the following:

  1. The client requests a token from the server, either by providing a password or another form of authentication (fingerprint, face-id, etc).
  2. The server provides a token. While there are two ways that the token is generated, in neither one would an attacker be able to generate their own tokens, because they would need the public and private key to generate and decode the token.
  • If this token is generated using secure encoding such as HS256, the token is generated using a securely stored secret and also checked against that secret
  • If using public key encryption such as RSA, the token is generated using the private certificate and checked again the public key

Now that we have explained what tokens are and how they are secured, we will continue to explain how they are used in the context of a web application.

JWTs are popular because they are self-contained and are stored client side — that means that the data of the session does not need to be stored server-side, as they would be in, for example, sticky sessions

Traditional middleware approach for authentication

When working with web services, the industry standard for implementing authentication is through middleware. A middleware is simply a function that is executed by the server at a specific time in the request lifecycle. This means that a middleware can be executed for some resources and not for others. This is useful in the context of authentication since you may want to have some resources protected and other resources public.

A middleware has other applications besides authentication, including, among other uses, logging or error handling.

In the example below, we can see a simple middleware using Express.js. This middleware just logs a message.

import * as express from 'express'

const app = express()

app.get('/posts', (req, res) => {
  res.send('posts')
})

app.use((req, res, next) => {
  console.log('message')
})

app.get('/protected-posts', (req, res) => {
  res.send('protected posts')
})

app.listen(3000, () => {
  console.log('listening on port 3000')
})

The key method in the middleware is the next() method. This method signals to Express.js to continue on with the request lifecycle.

Using middleware and JWT functionality together with verify

Now we need to somehow combine this middleware functionality with the JWT functionality. To achieve this, we will use a function from the JSON web token library called verify.

The verify function has the following signature:

jwt.verify(token, secretOrPublicKey, [options, callback])

It takes an encrypted token and a secret. If the token is valid, meaning the content has not been tampered with, the function will return the unencrypted token on the second parameter of the callback; if the token is invalid the function will return an error in the first parameter to the callback.

import * as express from 'express'
import * as jwt from 'jsonwebtoken'

const app = express()

app.get('/posts', (req, res) => {
  res.send('posts')
})

app.use((req, res, next) => {
  const { authorization } = req.headers
  jwt.verify(authorization, 'secret', (err, decodedToken) => {
    if (err || !decodedToken) {
      res.status(401).send('not authorized')
      return
    }
    next()
  })
})

app.get('/protected-posts', (req, res) => {
  res.send('protected posts')
})

app.listen(3000, () => {
  console.log('listening on port 3000')
})

As stated in the name, verify takes a token and checks for its validity against the secret key, returning the decoded token or an error.

In this example, we see how we would use the verify function in conjunction with a middleware to protect all the resources declared after this middleware.

Authentication with GraphQL using graphql-yoga

graphql-yoga is an easy to use GraphQL server library that we will use for the remainder of the article because of its simple setup and a straightforward developer experience.

The authentication of a graphql-yoga server extends the discussed middleware paradigm and since graphql-yoga is built on top of apollo-server, and apollo-server is built from Express.js, we can even use the exact same middleware.

import { GraphQLServer } from 'graphql-yoga'
import * as jwt from 'jsonwebtoken'

const typeDefs = `type Query { hello(name: String): String! }`

const resolvers = {
  Query: {
    hello: (_, { name }) => `Hello ${name || 'World'}`,
  },
}

const server = new GraphQLServer({ typeDefs, resolvers })
server.express.use((req, res, next) => {
  const { authorization } = req.headers
  jwt.verify(authorization, 'secret', (err, decodedToken) => {
    if (err || !decodedToken) {
      res.status(401).send('not authorized')
      return
    }
    next()
  })
})
server.start(() => console.log('Server is running on localhost:4000'))

This way of handling authentication works but it does not play well with GraphQL clients, since it relies on HTTP codes. A better way would be to use GraphQLMiddlewares from graphql-yoga: they let us manage the additional functionality that is relevant to all of our resolvers.

We also could use AuthenticationError from apollo-server-core to print a message that can be handled by our GraphQL client of choice.

import { GraphQLServer } from "graphql-yoga";
import * as jwt from "jsonwebtoken";
import { AuthenticationError } from "apollo-server-core";

const typeDefs = `type Query { hello(name: String): String! }`;

const resolvers = {
    Query: {
        hello: (_, { name }) => `Hello ${name || "World"}`
}
};

const autheticate = async (resolve, root, args, context, info) => {
    let token;
    try {
        token = jwt.verify(context.request.get("Authorization"), "secret");
    } catch (e) {
        return new AuthenticationError("Not authorised");
    }
    const result = await resolve(root, args, context, info);
    return result;
};

const server = new GraphQLServer({
    typeDefs,
    resolvers,
    context: req => ({ ...req }),
    middlewares: [autheticate]
});

server.start(() => console.log("Server is running on http://localhost:4000"));

With this refactor the GraphQL client, connecting in this instance to graphql-yoga, would be able to be able to deal with an authentication error, in a graceful manner.

Authorization

We’ve delved a little into what authorization is. We can think of authentication as the first part of making our applications secure but once someone gains access to our system, it’s just the beginning of the story.

As in the previous section, we will look at how to implement authorization in a traditional REST API, and then we will circle back to GraphQL. For that, we first need to revisit the concept of claims.

At their core, claims represent the actions that a user is allowed to take inside the system. The best practice is to have verbs and resources. A verb on a claim is the kind of action that a user can take on a resource. Such verbs can be CRUD (create, read, update, delete) operations. Resources, on the other hand, can be anything from other users, posts, or pictures.

Some examples for claims could be: “delete posts”, that lets a user delete any post, or “update pictures”, that lets someone update any picture that has already been created.

The way to work with claims in a traditional REST API is to check at the beginning of every request for the appropriate claims. If the required claims are not present, the request does not continue, as you can see in this example.

import * as express from 'express'
import * as jwt from 'jsonwebtoken'
import { Request } from 'express'

const app = express()

app.get('/posts', (req, res) => {
  res.send('posts')
})

interface ReqClaims extends Request {
  claims?: string;
}

app.use((req: ReqClaims, res, next) => {
  const { authorization } = req.headers
  jwt.verify(authorization, 'secret', (err, decodedToken) => {
    if (err || !decodedToken) {
      res.status(401).send('not authorized')
      return
    }
    req.claims = decodedToken.claims
    next()
  })
})

app.get('/protected-posts', (req: ReqClaims, res) => {
  if (req.claims !== 'read-posts') {
    res.status(401).send('not authorized')
    return
  }
  res.send('protected posts')
})

app.listen(3000, () => {
  console.log('listening on port 3000')
})

Now let’s explore this concept with GraphQL.

import { GraphQLServer } from "graphql-yoga";
import * as jwt from "jsonwebtoken";
import { AuthenticationError } from "apollo-server-core";

const typeDefs = `type Query { hello(name: String): String! }`;

const resolvers = {
    Query: {
        hello: (_, { name }, ctx) => {
    if (ctx.claims !== "read-posts") {
        return new AuthenticationError("not authorized");
    }
    return `Hello ${name || "World"}`;
}
}
};

const authenticate = async (resolve, root, args, context, info) => {
    let token;
    try {
        token = jwt.verify(context.request.get("Authorization"), "secret");
    } catch (e) {
        return new AuthenticationError("Not authorised");
    }
    context.claims = token.claims;
    const result = await resolve(root, args, context, info);
    return result;
};

const server = new GraphQLServer({
    typeDefs,
    resolvers,
    context: req => ({ ...req }),
    middlewares: [autheticate]
});

server.start(() => console.log("Server is running on http://localhost:4000"));

The problem with this approach is that we need to remember to protect each of our resolvers with claims. When we have a small number of claims this approach is not a problem but when we get into combinations of claims it quickly gets out of hand.

Using GraphQL Shield

A better way to implement the claims functionality is with graphql-shield, a tool that helps you create a permission layer in a web application.

With graphql-shield we need to provide a set of rules for every resolver in our schema and the decoded token to each rule. That rule can decide if the user is authorized or not.

GraphQL Shield translates the concept of claims into rules. A rule in graphql-shield protects one part of the GraphQL schema.

As seen in the example below, you’ll need first need to pass the appropriate rule to access any part of the GraphQL schema.

import { GraphQLServer } from "graphql-yoga";
import { rule, shield, and, or, not } from "graphql-shield";
import * as jwt from "jsonwebtoken";

const typeDefs = `type Query { hello(name: String): String! }`;

const resolvers = {
    Query: {
        hello: (_, { name }) => `Hello ${name || "World"} `
}
};

// Auth

function getClaims(req) {
    let token;
    try {
        token = jwt.verify(req.request.get("Authorization"), "secret");
    } catch (e) {
        return null;
    }
    return token.claims;
}

// Rules

const isAuthenticated = rule()(async (parent, args, ctx, info) => {
    return ctx.claims !== null;
});

const canReadposts = rule()(async (parent, args, ctx, info) => {
    return ctx.claims === "read-posts";
});

// Permissions

const permissions = shield({
    Query: {
        hello: and(isAuthenticated, canReadposts)
    }
});

const server = new GraphQLServer({
    typeDefs,
    resolvers,
    middlewares: [permissions],
    context: req => ({
        ...req,
        claims: getClaims(req)
    })
});

server.start(() => console.log("Server is running on http://localhost:4000"));

Conclusion

We’ve learned how, by using web tokens and claims, we can better secure our GraphQL API for multiple users with different kinds of permissions, but authentication and authorization is just one piece of the security puzzle. To further protect your APIs and the servers that host them checkout out this wiki from the Open Web Application Security Project.

#graphql #jwt #json #javascript #webdev

GraphQL Authentication using JSON Web Tokens
8.45 GEEK