Categories
GraphQL Node.js

How to Implement GraphQL in Microservices Using Node.js – Part 4

“Human security depends on a system where each rational individual calculates that it is more profitable not to rebel.”

― Mark Gough

Introduction

In the previous posts, we have learned about implementing GraphQL and creating queries and mutations. Should you get to this post first, I recommend you to read the previous posts here first:

This is the fourth part of “How to Implement GraphQL in Microservices using Node.js”, and in this part, we will learn about implementing authentication through GraphQL. Because, without this a secured server will deny requests sent from GraphQL server, even though the requests can be accepted without going through the GraphQL server.

I will provide the updated mock server with JWT Authentication to ease you in setting up for learning. And, at the end of this post, I will provide a working source code.

Let’s begin.

Requirements

Here are the requirements in this tutorial:

  • The previous GQL project
    It’s recommended to clone this project we created from the previous tutorial, and name it as cli-nodejs-gql-4. If you want to add the code directly, it’s fine too.
  • This mock service source code
    If you prefer to build another REST API on your own, you are free to do so. But, why not save time? If you do, after cloning the project, run npm i and explore the endpoints defined in index.js to make sure it works well.

Part 4 – Authentication

I assume that you already know about authentications. And the one I use here is by authenticating a JWT token obtained from logging in.

The approach I use is by passing the authentication from the GraphQL server to the mock service. Therefore, the mock service will be the one that authenticate the payloads sent from the front while GraphQL server do nothing but passing only.

By doing this, we make sure that the GraphQL server has only one job: to resolve query and mutation requests from the front.

Login Mutation

We will start by creating a mutation for admin login. The logic for logging in an admin is already in the mock service, so we can focus on creating the GraphQL resolver.

graphql/admin/…

First, we create the graphql/admin/...directories just like users, transactions, and banks before:

Directory structure for admin query.

The content in admin/index.js is the same as banks/index.js. And admin/queries/index.js is also the same with banks/queries/index.js.

Let’s create the schemas for admin in admin/queries/schemas.gql:

module.exports = `
  type AdminLogin {
    status: String!
    message: String
    token: String!
  }
`;

We call it AdminLogin, and it has three properties: status, message, token. Only status and token are required to be returned from the mock service. message enables us to understand better what status means.

Next, we create the AdminLogin resolver in admin/queries/resolvers:

const AdminLogin = {};

module.exports = { AdminLogin };

Yeah, it does nothing, because it does not need to get data from others.

_root/mutation/resolvers.js and _root/mutation/schemas.gql

Logging in requires a username and password, so we have to create a new mutation in _root/mutation/schemas.gql and _root/mutation/resolvers.js.

Here is the snippet on adding a login mutation:

module.exports = `
  type Mutation {
    # Banks
    ...
    # Users
    ...
    # Transactions
    ...
    # Login
    login(username: String!, password: String!): AdminLogin
  }
`;

We just need to add another line: login. It requires two parameters: username and password. The response from this request will be AdminLogin, which we have written on admin/queries/schemas.gql.

Next, we write the mutation resolver in _root/mutations/resolvers.js:

// _root/mutations/resolvers.js
...

const banks = {
    ...
};

const users = {
    ...
};

const transactions = {
    ...
}

const admin = {
    login: async (_, data) => {
        return post(`${baseUrl}/admin/login`, data);
    }
}

const Mutation = {
    ...,
    ...admin
};

module.exports = { Mutation };

Pretty much the same with the other mutations, we just need to pass data to POST <baseUrl>/admin/login. Then register the admin resolver to Mutation object.

Test the code

Run npm start on both GraphQL server (cil-nodejs-gql-4) and the mock service. Open GraphQL Playground from http://localhost:4000/graphql.

In the query field, write this mutation:

mutation Login ($username:String!, $password:String!){
  login(username:$username, password: $password){
    status,
    message,
    token
  }
}

Then in query variables field, put both username and password as an object:

{
  "username": "admin",
  "password": "admin123"
}

Send the request, and you should get the response similar to this:

Login Mutation Response

The string token will be used later as authentication for all requests we created before.

Nice.

Use token as Authentication in Headers

We got the token. Now, we need to use them for our requests. In the mock service I updated all mutations and queries, except login, to require authentication before processing the request.

You will see this kind of message if you try to send a query request to get banks without authentication:

Sending a query request without authentication.

And here is the text version from the response:

{
  "errors": [
    {
      "message": "Error: Request failed with status code 401",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "banks"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Error: Request failed with status code 401",
            "    at get (/Users/19057992/Documents/training/cil-nodejs-gql-4/connectors/httpConnector.js:9:15)",
            "    at processTicksAndRejections (internal/process/task_queues.js:97:5)"
          ]
        }
      }
    }
  ],
  "data": {
    "banks": null
  }
}

The message shows that it returns a request failed status with code 401. Code 401 is “Unauthorized”, which means it needs some sort of authentication to authorise the access. And here is where our token plays its part.

We will update a resolver for getting a list of Banks as an example.

graphql/baseResolver.js

First, we create a baseResolver.js, which will be used as the general endpoint to get the auth header value, and then import httpConnector.js to send the request to the mock service:

// graphql/baseResolver.js

const { get } = require('../connectors/httpConnector');
const baseUrl = 'http://localhost:4001';

const createGetListUrl = (url, emptyValue = []) => {
    return async (_, params, context) => {
        const { auth = '' } = context.headers;
        return await get(`${baseUrl}/${url}`, { headers: { auth } }) || emptyValue;
    }
};

const createGetUrl = (url, emptyValue = {}) => {
    return async (_, params, context) => {
        const { auth } = context.headers;
        return await get(`${baseUrl}/${url}/${params.id}`, { headers: { auth } }) || emptyValue;
    }
};

module.exports = {
    baseUrl,
    createGetListUrl,
    createGetUrl
}

We import get() from httpConnector.js, then set baseUrl. Then we create a new function createGetListUrl(), which will return a generic asynchronous function as a resolver which requests get()from httpConnector.js.

The returned function will get auth from context and send it to get() as a property of { headers }.

The same goes with createGetUrl(), which gets params.id from the returned asynchronous function.

connectors/httpConnector.js

So, we need to modify httpConnector.js also:

const axios = require('axios');

const get = async (url, headers = {}) => {
    try {
        const response = await axios.get(url, headers);
        return response.data;
    } catch (error) {
        ...
    }
    return null;
}
...

Add a headers parameter on get() and axios.get().

_root/queries/resolvers.js

Now, we will refactor _root/queries/resolvers.js to use baseResolver.

const { 
  createGetUrl, 
  createGetListUrl 
} = require('../../baseResolver');

const Query = {
  banks: createGetListUrl(`banks`),
  bank: createGetUrl(`banks`),
  ...
};

module.exports = { Query };

We refactor this module to use baseResolver we created before. The functions we need are createGetUrl() and createGetListUrl().

Next, we change the get() function in banks and bank to call createGetListUrl() and createGetUrl() respectively. And we put “banks” as the argument for both functions.

Test the code

OK, let’s run both the mock service and GraphQL server. Open the GraphQL Playground and do these steps:

  1. Copy the token from the Login mutation, you have to send the username and password first, of course.
  2. Reopen the Banks query.
  3. In the HTTP Headers field, put this:
{
  "auth": "<the_token_you copied_before>"
}

Lastly, run the query. The result should look like the screenshot below:

Query Banks with Authentication. Remember to put the HTTP headers!

Now, you can do the same for getting a bank.

Practice Time!

We have created a sample of adding authentication through GraphQL to the mock service for getting a list of banks and a bank detail. How about users and transactions? You can try to add them and see if it works.

Users and Transactions Resolvers

If you tried to add these both resolvers, you might find that it did not work just like banks. Most likely you will encounter this error:

{
  "errors": [
    {
      "message": "Error: Request failed with status code 401",
      "locations": [
        {
          "line": 5,
          "column": 5
        }
      ],
      "path": [
        "users",
        1,
        "bank"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Error: Request failed with status code 401",
            "    at get (/Users/19057992/Documents/training/cil-nodejs-gql-4/connectors/httpConnector.js:9:15)",
            "    at processTicksAndRejections (internal/process/task_queues.js:97:5)"
          ]
        }
      }
    }
  ],
  "data": {
    "users": null
  }
}

Look at the path properties. It shows users, 1, and bank. This basically means the error happen when getting the bank details of the first user, or in array form: user[0]. What is going on here? It worked before right?

Let’s check user/queries/resolvers.js to see what happens. And here is a snippet of the file:

const { get } = require('../../../connectors/httpConnector');
const baseUrl = 'http://localhost:4001';

const User = {
  bank: async (user) => await get(`${baseUrl}/banks/${user.bankId}`) || {}
};

module.exports = { User };

The bank endpoint is still without header authentication. So, we just need to refactor the code just like what we did on _root by importing createGetUrl() from baseResolvers. See this snippet:

const { createGetUrl } = require('../../baseResolver');

const User = {
    bank: async (user, _, context) => {
        const getBank = createGetUrl('banks');
        return getBank(_, { id: user.bankId }, context);
    }
};

module.exports = { User };

We still need to get the bankId from user, so we get user the parameter on the bank resolver function. Then, we create a getBank() function just like when we query banks before.

Lastly, we call the getBank() function with the correct parameters. We can currently ignore the first parameter as we won’t be using it. The second parameter is params, therefore we can assign user.bankId as the value of id in an object. The third and final parameter is the same context passed from the function. This is important because we need the auth header passed through context.

Test the Code

Run npm start on both the mock service and the GraphQL Server. Then, run GraphQL Playground and query Users with authentication like query Banks before. It should work this time.

Practice Time!

Now, you should be able to refactor the transactions query with the same steps just like on users. You can compare your code with my source code at the end of the post.

What about mutations?

Same approach. You just need to add createPostUrl(), createPatchUrl() and createDeleteUrl() on baseResolver.js.

Then refactor _root/mutations/resolvers.js to import from baseResolver and the asynchronous functions for every mutations.

Lastly, of course, test the code as usual 🙂

Conclusion

Finally we reached the end of the post. You should have learned about:

  • Creating a login mutation
  • Refactor queries to support auth.
  • Passing headers through context to detail resolvers.
  • Refactor mutations to support auth.

And here is the source code for this post. Let me know if there’s something I miss or can be improved. Happy coding!

“We live in a world that has walls and those walls need to be guarded by men with guns.”

― Aaron Sorkin, A Few Good Men

By Ericko Yap

Just a guy who is obsessed to improve himself. Working as a programmer in a digital banking company. Currently programming himself in calisthenics, reading books, and maintaining a blog.

Leave a Reply

Your email address will not be published. Required fields are marked *