DEV Community šŸ‘©ā€šŸ’»šŸ‘Øā€šŸ’»

Cover image for GraphQL Authentication and Authorization in Node.js
Francisco Mendes
Francisco Mendes

Posted on

GraphQL Authentication and Authorization in Node.js

In the previous article we created a simple GraphQL api from scratch and in today's article I will explain how we can implement a simple authentication and authorization system.

In today's article we are going to work with the user's permissions, first we will create the account, then we will go to the login where we will get the Json Web Token and finally we will protect some queries and mutations (so that only the users with tokens can perform these operations).

that's a great ideia meme

For this we will make some changes in the code of the previous article but first of all we will install the necessary dependencies for today's article.

Required Libraries

  • jsonwebtoken - this dependency will be responsible for creating the json web tokens, as well as checking their readability
  • argon2 - this dependency will hash and compare the passwords
  • graphql-middleware - this dependency will allow us to add additional functionality to various resolvers efficiently
  • graphql-shield - this dependency makes it possible to establish roles and permissions in our api in an easy and intuitive way
  • dotenv - this dependency loads environment variables from a .env file into process.env

Installation

Now let's proceed to install the dependencies that were mentioned earlier:

# NPM
npm install jsonwebtoken argon2 graphql-middleware graphql-shield dotenv

# YARN
yarn add jsonwebtoken argon2 graphql-middleware graphql-shield dotenv

# PNPM
pnpm add jsonwebtoken argon2 graphql-middleware graphql-shield dotenv
Enter fullscreen mode Exit fullscreen mode

User Database Model

Our first step will be to create the user entity in our database. In this article I didn't establish any relationships between the models (User and Dog), but if you want to do so it's completely valid.

// @/src/db/models/User.js

import Sequelize from "sequelize";

import { databaseConnection } from "../index.js";

export const UserModel = databaseConnection.define("User", {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true,
    allowNull: false,
  },
  username: {
    type: Sequelize.STRING,
    allowNull: false,
  },
  password: {
    type: Sequelize.STRING,
    allowNull: false,
  },
});
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, our model only has four properties (id, username and password), now just add it to the input file of our models:

// @/src/db/models/index.js

export * from "./Dog.js";
export * from "./User.js"; // <- This line was added
Enter fullscreen mode Exit fullscreen mode

Now with our updated models, let's move on to the next step which will be to create some utilities.

Create Utilities

The reason for creating these utilities is due to the fact that we are going to use them several times during the development of our api and it is not productive to be constantly decorating them, it is better to create a simple standardization.

In the same way that if in the future we want to change a certain dependency, instead of going to all the resolvers, just change it in the util.

Enough talk, let's now create the following utilities (each one corresponds to a different file):

// @/src/utils/hashPassword.js
import { hash } from "argon2";

export const hashPassword = async (password) => {
  return await hash(password);
};

// @/src/utils/verifyPassword.js
import { verify } from "argon2";

export const verifyPassword = async (hash, password) => {
  return await verify(hash, password);
};

// @/src/utils/signToken.js
import jwt from "jsonwebtoken";

export const signToken = (data) => {
  return jwt.sign(data, process.env.JWT_SECRET);
};

// @/src/utils/verifyToken.js
import jwt from "jsonwebtoken";

export const verifyToken = (token) => {
  return jwt.verify(token, process.env.JWT_SECRET);
};
Enter fullscreen mode Exit fullscreen mode

With our utilities created, we can create their entry file:

// @/src/utils/index.js

export * from "./hashPassword.js";
export * from "./verifyPassword.js";
export * from "./signToken.js";
export * from "./verifyToken.js";
Enter fullscreen mode Exit fullscreen mode

Now with the utilities created, we can move on to defining our graphql type definitions.

so far so good meme

Types and Resolvers

Similar to what we did in the previous article, we will now define our type definitions, however in this case we will only define the user's.

# @/src/graphql/typeDefs/Mutations/User.gql
type User {
  id: ID
  username: String
  password: String
  token: String
}

input userInput {
  username: String!
  password: String!
}

type Mutation {
  register(input: userInput): User
  login(input: userInput): User
}
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, we created the login and registration mutations. Now let's go to the processinger to create the corresponding resolvers.

First we will work on the registration, for that we will import the model from the user's database, as well as we will import the util of signToken and hashPassword. Then we will get the values of the input object from the arguments and we will hash the password sent by the user.

Finally we will add the data in the database table and we will return the necessary properties in the response, such as the user id, username, password and token. In the token payload we will only store the user id.

// @/src/graphql/resolvers/Mutations/register.js
import { UserModel } from "../../../db/models/index.js";
import { signToken, hashPassword } from "../../../utils/index.js";

export const register = async (parent, args, context) => {
  const { password, ...rest } = args.input;

  const hashedPassword = await hashPassword(password);

  const result = await UserModel.create({ ...rest, password: hashedPassword });

  return {
    id: result.id,
    username: result.username,
    password: result.password,
    token: signToken({ userId: result.id }),
  };
};
Enter fullscreen mode Exit fullscreen mode

Then we can start working on the user login and similar to the previous solution, we will first import the user's database model and the necessary utils, such as signToken and verifyPassword.

Then we will get the data from the input object of our mutation arguments and we will check if the user exists in the database. After that, we will check if the password entered by the user is the same as the one stored in the database. Finally, we will return in the response only the user id, as well as the username and the token.

// @/src/graphql/resolvers/Mutations/login.js
import { UserModel } from "../../../db/models/index.js";
import { signToken, verifyPassword } from "../../../utils/index.js";

export const login = async (parent, args, context) => {
  const { password, username } = args.input;

  const result = await UserModel.findOne({ where: { username } });

  const isValidPassword = await verifyPassword(result.password, password);

  if (!isValidPassword) {
    throw new Error("Invalid password");
  }

  return {
    id: result.id,
    username: result.username,
    token: signToken({ userId: result.id }),
  };
};
Enter fullscreen mode Exit fullscreen mode

With our resolvers finished, we can add them to the mutations entry file.

// @/src/graphql/resolvers/Mutations/index.js
export * from "./addDog.js";
export * from "./updateDog.js";
export * from "./deleteDog.js";
export * from "./register.js"; // <- This line was added
export * from "./login.js"; // <- This line was added
Enter fullscreen mode Exit fullscreen mode

Now we can move on to the next step, which is to create the permissions for our api.

i will show you how it works gif

Create Rules/Permissions

In this article I will create just one permission, in which we will check whether or not the user is sending the token in the headers and if it is valid.

First, we are going to create our permission, in which we are going to import the rule function from graphql-shield and we are going to check if the authorization header is being sent, if not, the user will not be able to perform any action on the resolver.

Otherwise we will get the authorization header token and we will verify it using the verifyToken util. If the user is legible, he can perform the desired operation.

// @/src/guards/rules/isAuthorized.js

import { rule } from "graphql-shield";

import { verifyToken } from "../../utils/index.js";

export const isAuthorized = rule()(async (parent, args, ctx, info) => {
  const { authorization } = ctx.request.headers;
  if (!authorization) {
    return false;
  }

  const token = authorization.replace("Bearer", "").trim();

  const { userId } = verifyToken(token);

  return !!userId;
});
Enter fullscreen mode Exit fullscreen mode

Now we can create the entry file of our rules and let's import the one we just created.

// @/src/guards/rules/index.js
export * from "./isAuthorized.js";
Enter fullscreen mode Exit fullscreen mode

We still need to specify which queries and mutations we want to add permissions and which rules we want to associate in the resolvers. In this article I decided to protect some mutations, such as:

// @/src/guards/index.js

import { shield } from "graphql-shield";

import { isAuthorized } from './rules/index.js'

export const permissions = shield({
  Query: {},
  Mutation: {
    deleteDog: isAuthorized,
    addDog: isAuthorized,
    updateDog: isAuthorized,
  },
});
Enter fullscreen mode Exit fullscreen mode

Now with our rules created, we can make some adjustments to some files.

Small Adjustments

First we need to make some small changes to our Apollo Server instance, like implementing the middleware in our schema.

For this we will import the applyMiddleware function from the graphql-middleware dependency, which will have our schema and the various middlewares that can be added as arguments.

In the createApolloServer function, we only receive an argument to which we destruct to get the app and the schema. However this time we are going to add a new argument, called middleware and this argument will be an array.

Then we will create a variable called schemaWithPermissions to which the value of the applyMiddleware function will be associated. Finally, just associate the schemaWithPermissions variable to the ApolloServer schema property. Like this:

// @/src/apollo/createApolloServer.js
import { ApolloServer } from "apollo-server-fastify";
import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";
import { applyMiddleware } from "graphql-middleware";  // <- This line was added

// midlewares argument was added to the createApolloServer function
export const createApolloServer = (midlewares, { app, schema }) => {
  const schemaWithPermissions = applyMiddleware(schema, ...midlewares);  // <- This line was added

  return new ApolloServer({
    schema: schemaWithPermissions,  // <- This line was changed
    context: ({ request, reply }) => ({
      request,
      reply,
    }),
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer: app.server }),
      {
        serverWillStart: async () => {
          return {
            drainServer: async () => {
              await app.close();
            },
          };
        },
      },
    ],
  });
};
Enter fullscreen mode Exit fullscreen mode

Last but not least, we need to go to our startApolloServer function and make some final changes. First let's import dotenv so we can load the environment variables as well as our permissions.

Then let's initialize dotenv and pass the permissions inside an array as the first argument of the createApolloServer function. Thus:

// @/src/server.js
import { makeExecutableSchema } from "@graphql-tools/schema";
import fastify from "fastify";
import dotenv from "dotenv";  // <- This line was added

import { typeDefs, resolvers } from "./graphql/index.js";
import { permissions } from "./guards/index.js";  // <- This line was added
import { createApolloServer } from "./apollo/index.js";
import { databaseConnection } from "./db/index.js";

export const startApolloServer = async () => {
  dotenv.config();  // <- This line was added

  const app = fastify();

  const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
  });

  // Our permissions are passed in the middleware array argument
  const server = createApolloServer([permissions], { app, schema });
  await server.start();

  await databaseConnection.sync();

  app.register(server.createHandler());

  await app.listen(4000);
};
Enter fullscreen mode Exit fullscreen mode

Our implementation has been completed and now you can use GraphQL Playground or Apollo Studio to perform your queries and mutations, not forgetting that you will need to get the token at login or register so that it can be sent in the headers so that it is possible to perform an operation (such as inserting a dog into the database).

If you made it this far, you can access the Github repository by clicking on this link.

The End

I hope you enjoyed this little series, I've tried to keep everything as simple as possible so that it's easy to implement more things from here or just adjust it to suit your needs. I hope it was helpful. šŸ‘Š

see you soon gif

Top comments (1)

Collapse
 
nasimuddin profile image
Nasim Uddin

Graphql-Shield is currently unmaintained. What should I use now. Do you have any idea?

12 Rarely Used Javascript APIs You Need

>> Check out this classic DEV post <<