close icon
daily.dev platform

Discover more from daily.dev

Personalized news feed, dev communities and search, much better than what’s out there. Maybe ;)

Start reading - Free forever
Continue reading >

Everything you need to know about GraphQL Authentication and Authorization

Everything you need to know about GraphQL Authentication and Authorization
Author
Chidume Nnamdi
Related tags on daily.dev
toc
Table of contents
arrow-down

🎯

In this post, we are going to learn about both Authorization and Authentication in GraphQL.

Most developers coming from the REST API world would find this hard to comprehend, like how is it even possible. Authorization in GraphQL is different and can be done in many ways, but authentication is the same as we have in REST.

Let's see them both in the below sections.

Authentication

Authentication is the process of verifying the identity of a user in a system. To verify the user's identity, the user must supply a token or a key which the server will use to retrieve the user's info from its databank. if the user's info is in the databank then, the user is authenticated, that is, the user is allowed inside the system. If not, the server will tell the user that he is not authenticated and won't be allowed access to the system.

Now, if a user is not authenticated, he has to request for registration via a registration form provided by the system. If the user's token has expired, he has to request for a new token via the system's login form.

There are ways in which we can authenticate users but the easiest and the most secured way is via JWT (JSON Web Token).

JWT

JWT is an open standard way in which information can be securely communicated between two entities. The information being transmitted is in a compact JSON object.

JSON has three parts:

  • Header: This contains the type of token used in signing the JWT and the signing algorithm being used, e.g HS256, etc.
  • Payload: This contains a statement about an entity. This statement is called a claim. There are different types of claims, registered, public or private claims.
  • Signature: This is generated by signing the encoded header, encoded payload, a secret, and the algorithm specified in the header.

The final JWT output is three Base64-URL strings separated by dots.


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL3NwYWNlYXBpLmNvbS9ncmFwaHFsIjp7InJvbGVzIjpbImFzdHJvbmF1dCJdLCJwZXJtaXNzaW9ucyI6WyJyZWFkOm93bl91c2VyIl19LCJpYXQiOjE1OTQyNTI2NjMsImV4cCI6MTU5NDMzOTA2Mywic3ViIjoiNjc4OTAifQ.Z1JPE53ca1JaxwDTlnofa3hwpS0PGdRLUMIrC7M3FCI

You must be wondering what do we do with this sting of random texts and numbers in the authentication. The data in the custom claim in the payload is generated for a particular when the user signs up in our system. The generated JWT is sent to the client.

The client stores this JWT in the browser, JWTs are mostly saved in the localStorage, though it depends on the choice of the developer. For each request to the server, this JWT is sent along with each request. There are many forms you can use to send the JWT, but most commonly it is appended in the Authorization header in a Bearer type authentication just like this:


{

  "headers": {

    "Authorization": "Bearer xxxxx.yyyyy.zzzz"

  }

}

The server will then retrieve the JWT token from the Authorization header, decode it to verify the user's identity, to check that the user is already registered in the system.

Authenticating GraphQL is quite simple. We make use of queries and mutations. For a user to have access he has to register first, that's why we have register pages on almost all websites. The registration hits a particular endpoint, for example, it is usually like this https://URL_HERE/register API in a REST endpoint. For signing in, it usually in a URL like this: https://URL_HERE/login in a REST endpoint.

In GraphQL, it is similar but it is in the form of query and mutation because it does have URL endpoints like in endpoints, it communicates in one URL, for e.g https://URL_HERE/graphql. For signing in to our GraphQL server, we will have the login mutation like this:


mutation login($email: String, $password: String!) {

  signIn(email: $email, password: $password)

}


This mutation comes from the client-side, then the GraphQL login resolver will be called on the server to handle the login.

For registration, we can have a register mutation like this:


mutation register($email: String!, $username: String!, $password: String!) {

  signUp(email: $email, username: $username, password: $password)

}


On the server, the register resolver function will handle this. It will set up the user in the database using the credentials passed in.

So, this is how authentication is done in GraphQL. Now, we will demonstrate this authentication process by building an Express.js-backed Nodejs GraphQL server.

This server will be a blog server with GraphQL endpoints. Users can be able to:

  • read a blog post
  • see lists of blog posts
  • post a blog post

Now, we want that only authenticated users can post a blog post, and only authenticated users can view a paid blog post.

Now, let's create the project.

First, we will create a folder auth-gpl:


mkdir auth-gpl

Move into the folder:


cd auth-gpl

Now, we initialize a Node environment.


npm init --y

Now, we install the dependencies:


npm i apollo-server-express bcrypt express graphql jsonwebtoken

  • apollo-server-express: This is the Express and Connect integration of GraphQL Server. It simplifies the connection of Express.js with a GraphQL server easily and seamlessly.
  • bcrypt: This library will help us hash passwords.
  • express: Minimalistic web framework for Node.js.
  • graphql: The JavaScript reference implementation for GraphQL, a query language for APIs created by Facebook.
  • jsonwebtoken: The library will help us generate, verify and sign JWT tokens.

Create resolvers.js and schema.js files:


touch resolvers.js schema.js

The resolvers.js file will contain our GraphQL endpoint resolvers, the schema.js will contain our GraphQL schema.

In this app, we will query:

  • for a blog post blogPost(id)
  • for all the blog posts blogPosts()
  • for a user user(id)
  • for all users users()

Our mutations will be:

  • add a blog post addBlogPost(...)
  • sign in signIn(...)
  • sign up signUp(...)

Resolvers

First, we create mock data for blog posts.


touch data.json

Fill it in with the below data:


{

  "blogPosts": [

    {

      "id": "_1",

      "title": "Intro to Alpinejs",

      "body": "Alpine.js tutorial content",

      "postImage": "no_image"

    },

    {

      "id": "_2",

      "title": "Intro to React.js",

      "body": "React.js tutorial content.",

      "postImage": "no_image"

    },

    {

      "id": "_3",

      "title": "Intro to Angular",

      "body": "Angular tutorial content",

      "postImage": "no_content"

    }

  ],

  "users": []

}

The users array holds the users in our app. The blogPosts holds all the blog posts published in our app.

In our resolvers.js file, create an object literal, inside the object literal create a Query object.


const resolvers = {

  Query: {},

};

nside the Query object we will define functions that will have the same names as our intended query name.

Let create the function to handle the blogPosts() query.


const { blogPosts, users } = require("./data.json");



const resolvers = {

  Query: {

    blogPosts(parent, args, context, info) {

      const blgPosts = blogPosts;

      return {

        nodes: blgPosts,

        aggregate: {

          count: blgPosts.length,

        },

      };

    },

  },

};

Let's create all functions for each query.


const { blogPosts, users } = require("./data.json");



const pubsub = new PubSub();



const resolvers = {

  Query: {

    blogPosts(parent, args, context, info) {

      const blgPosts = blogPosts;

      return {

        nodes: blgPosts,

        aggregate: {

          count: blgPosts.length,

        },

      };

    },

    blogPost(parent, args, context, info) {

      const blgPosts = blogPosts;

      const id = args.id;

      return blgPosts.find((r) => r.id == id);

    },

    user(parent, args, context, info) {

      const { id } = args;

      return users.find(({ userId = id }) => userId == id);

    },

    users() {

      return users;

    },

  },

};



module.exports = resolvers;

Quite clear, the blogPosts function returns the blog posts in the edge and the total number of the blog posts in the aggregate.

The blogPost gets the id of the blog post to be retrieved from the args param and uses Array#find method to find the blog post and return it.

The user does the same thing with the id as blogPost, then, uses it to find the particular user and return it.

The users returns all the user's array.

Let's add mutation.

Create a Mutation object next to the Query object.


const resolvers = {

  Query: {

    // ...

  },

  Mutation: {},

};

Let's add addBlogPost function.


const resolvers = {

  Query: {

    // ...

  },

  Mutation: {

    addBlogPost(parent, args, context, info) {

      const { title, body, postImage } = args;

      const newBlogPost = {

        id: Date.now(),

        title,

        body,

        postImage,

      };

      blogPosts.push(newBlogPost);

      return newBlogPost;

    },

  },

};

The details of the blog post are destructured from the arg param and pushed to the blogPosts array.

Let's create the signIn function.

Now, the mutation for sign-in will carry the email and password of the user. We will destructure them from the args param, we get the user in the users array, then use bcrypt module to compare the passwords from the passed-in one and the one already in our users array.

If the passwords match we use the jsonwebtoken to create a JWT token from the user's id and a secret (for this post, our secret will be "nnamdi"), then, the JWT is returned to the user.

The code:


const resolvers = {

  Query: {

    // ...

  },

  Mutation: {

    addBlogPost(parent, args, context, info) {

      //...

    },

    async signIn(parent, args, context, info) {

      const { email, password } = args;



      const user = users.find(({ userEmail = email }) => userEmail == email);



      // if no user is found, throw an authentication error

      if (!user) {

        return { error: "User not found." };

      } else {

        // if the passwords don't match, throw an authentication error

        const valid = await bcrypt.compare(password, user.password);

        if (!valid) {

          return { error: "Password does not match." };

        } else {

          // create and return the json web token

          return {

            token: jwt.sign(

              { id: user._id },

              "nnamdi" /* process.env.JWTSECRET */

            ),

          };

        }

      }

    },

  },

};

Let's add the signUp function.

The signUp will expect the email, password, and username from the args param.

Next, the email will be checked to know it doesn't already exist. Then, a hash of the password will be created using the bcrypt module. Passwords are not stored plainly in the database, it is the hashes of the passwords that are stored.

Next, the user object is generated from the email, hashed password, generated id (we are using the Date.now(), there are standard ways to generate unique ids), and the username. This object is pushed to the users array. Then, jsonwebtoken will generate a JWT from our id and our secret "nnamdi". We used id for signing the token so that during verification for authorized uses access, we can verify the JWT and return the id, this id will be passed to the context so all queries and mutations can get and use it.

Finally, The JWT token will be returned to the user.

Like we already said, this JWT will be sent alongside each mutation/query so the system verifies each request before letting it pass through. We will see how that is done now.

But, let's add our schema first. Open schema.js

Schema defines the shape our queries, mutations and their result will take. It is just like data-typing.

All queries and mutations schema goes inside the type Query {} and type Mutation {} respectively.


const typeDefs = gql`

  type BlogPost {

    _id: String

    title: String

    body: String

    postImage: String

  }



  type User {

    id: Int

    username: String

    email: String

    password: String

  }



  type Aggregate {

    count: String

  }



  type BlogPosts {

    nodes: [BlogPost]

    aggregate: Aggregate

  }



  type SignInResponse {

    token: String

    error: String

  }



  type SignUpResponse {

    token: String

    error: String

  }



  type Query {

    blogPosts: BlogPosts

    blogPost(id: String): BlogPost

    user(id: Int): User

    users: [User]

  }



  type Mutation {

    addBlogPost(title: String, body: String, postImage: String): BlogPost

    signUp(email: String, username: String, password: String): SignUpResponse

    signIn(email: String, password: String): SignInResponse

  }

`;

See, they describe the shape of all the queries, mutations, and return values.

Now, open index.js. Setup the Express.js and GraphQL server using ApolloServer:


const express = require("express");

const { ApolloServer } = require("apollo-server-express");

const { createServer } = require("http");



const app = express();

const server = new ApolloServer({});



server.applyMiddleware({ app });



const httpServer = createServer(app);



httpServer.listen(3000, () => {

  console.log("connected!");

});


The Express.js instance is at the app variable. The ApolloServer instance is created and stored in the server variable.

The applyMiddleware method from ApolloServer applies itself to be a middleware in Express.js, this sets up the route /graphql, so all requests pass through it.

The http server instance is created off Express.js instance app and started at port 3000.

We need to import our resolvers and schemas and then, pass is in the ApolloServer constructor.


const typeDefs = require("./schema");

const resolvers = require("./resolvers");



const app = express();

const server = new ApolloServer({

  typeDefs,

  resolvers,

  playground: {

    endpoint: "http://localhost:3000/graphql",

    settings: {

      "editor.theme": "light",

    },

  },

});

The typedefs and resolvers exported from the resolvers.js and schema.js files respectively are passed in the object. The playground is a configuration for the playground theme and endpoint.

Now, we’ll need to verify that token on each request. We will do that by adding a context prop in the object arg in the ApolloServer constructor. The function in the context is called on each request to the server, so that makes an ideal place to verify each user before letting them in.

In this context prop, the function, we will grab the "Authorization: XXXXX" from the header of the request. Then, we verify the token there and add the user's id to the context. After this verification, each GraphQL resolver will have access to the user ID.

See the code:


...

const server = new ApolloServer({

  typeDefs,

  resolvers,

  context: ({ req }) => {

    // We will verify the user's identity here.

      if (req.headers && req.headers.authorization) {

        var auth = req.headers.authorization;

        var parts = auth.split(" ");

        var bearer = parts[0];

        var token = parts[1];

        if (bearer == "Bearer") {

          const user = getUser(token);

          if (user.error) {

            throw Error(user.msg);

          } else return { user };

        } else {

          throw Error("Authentication must use Bearer.");

        }

      } else {

        throw Error("User must be authenticated.");

      }

  },

  playground: {

    endpoint: "http://localhost:3000/graphql",

    settings: {

      "editor.theme": "light",

    },

  },

});

...

// get the user info from a JWT

const getUser = (token) => {

  if (token) {

    try {

      // return the user information from the token

      return jwt.verify(token, "nnamdi" /*process.env.JWT_SECRET*/);

    } catch (err) {

      // if there's a problem with the token, throw an error

      return { error: true, msg: "Session invalid" };

    }

  }

};


In the context, the authorization was retrieved and split into parts:


["Bearer:", "XXXXYYYY"];


The second part is the JWT token, so the second index 1 is referenced and passed to the getUser, function. This function uses jsonwebtoken's verify function to verify the JWT is valid, doing this returns the user ID used to sign it. This user ID is then returned by the context. All resolvers can then reference the user ID via context param.

Like this now, the context verifies all mutations and queries. We should not verify authentication when the user is trying to sign in, sign out, fetch a user, user, a blog post, or blog posts. We have to put a check for only when adding a blog post.


...

const server = new ApolloServer({

  typeDefs,

  resolvers,

  context: ({ req }) => {

    // We will verify the user's identity here.

    if (req.body.query.match("addBlogPost")) {

      if (req.headers && req.headers.authorization) {

        var auth = req.headers.authorization;

        var parts = auth.split(" ");

        var bearer = parts[0];

        var token = parts[1];

        if (bearer == "Bearer") {

          const user = getUser(token);

          if (user.error) {

            throw Error(user.msg);

          } else return { user };

        } else {

          throw Error("Authentication must use Bearer.");

        }

      } else {

        throw Error("User must be authenticated.");

      }

    }

  },

  playground: {

    endpoint: "http://localhost:3000/graphql",

    settings: {

      "editor.theme": "light",

    },

  },

});

...

The query/mutation is in the req.bod.query. We call the String#match function to check there is a addBlogPost text in the query, with this we know that the request is a addBlogPost mutation so we pass it in for verification.

We have seen how to implement authentication in a GraphQL server, now let's look at authorization.

Authorization

Authorization entails giving users levels of access to a system.

For example, in a bank, the bank manager has access to the bank vault, but not the bank workers. Also, each bank worker has different levels of access to the bank info and storage. A bank might not have access to the bank vault but might have access to the bank's strong room. Another bank worker might not have access to the bank's strong room.

So the access roles differ for each worker or user. This level of access is called Authorization.

In our blog post system, we can have roles like this:

  • admin
  • editor
  • reader

The admin has rights and access to all resources in the system and can execute all queries.

The editor have not all rights and access in the system. An editor can add a blog post but cannot delete a blog post, also an editor can add a user but cannot delete a user.

The reader can only execute the read queries like to read a blog post only, but cannot delete a blog post, edit a blog post, etc.

Authentication is an API-wide sort of authorization but in the case of a hierarchy of access, this will be done in the resolvers. The resolvers will check for the user's permission level before acting.

Let's add access-level authorization to our existing GraphQL example.

We can add role's access in the user's object like this:


{

  "users": [

    {

      "email": "",

      "password": "",

      "username": "",

      "role": ""

    }

  ]

}

The role can be admin, reader, or editor. The resolvers use the role attribute in the user object to determine the access level of the user.

Now, let's expand on our addBlogPost resolver to add authorization only for admin and editor roles.


addBlogPost(parent, args, context, info) {

      const userId = context.user;



      // get the user object

      const user = users.find(({ _userId = id }) => _userId == userId);

      const userRole = user.role;

      // check the user has an "admin" or "editor" role.

      if (userRole !== "admin" && userRole !== "editor")

        return {

          error: "You have no permission to perform this action",

        };



      const { title, body, postImage } = args;

      const newBlogPost = {

        id: Date.now(),

        title,

        body,

        postImage,

      };

      blogPosts.push(newBlogPost);

      return newBlogPost;

    },


That's authorization at play there.

The user Id is gotten from the context from its user property set by our context in the ApolloServer constructor. This is used to get the particular user from the users array. Since we know that the role of the user is kept in the role property of the user object, it is referenced and stored in the userRole variable and then, we checked if the value is either "admin" or "editor" so to allow the user add a new blog post.

If neither of the roles is the case then, an error is returned stating that the user is not authorized to perform the mutation.

the way we handled authorization isn't DRY at all. There are standard ways in which we can achieve role-based authorization using DRY principles. But at least, we got the point of authorization here.

Conclusion

We learned a lot about both authentication and authorization in a GraphQL API. Both are very easy to implement.

First, we started by learning what GraphQL is and why it is better than REST. Then, next, we learned about authentication and how it can be implemented in a GraphQL API using Express.js and APolloServer. Then, we saw how authorization can be added to the mix to introduce access-level authorization in the API.

It was exhilarating and awesome at the same time learning about both of them.

Citations

Why not level up your reading with

Stay up-to-date with the latest developer news every time you open a new tab.

Read more