Looking for a guide to access AWS Appsync Graphql api from NodeJS app via http request

Looking for a guide to access AWS Appsync Graphql api from NodeJS app via http request


-1

There is currently, to my knowledge, no one guide that walks through this process so I’d like to find one that can be extended upon and commented on by others who may have extra security "best practices" and the likes to contribute.

0

2 Answers
2


1

AWS Appsync API access via IAM user in NodeJS

What we’ll cover

  • Setting up your local env to build and deploy to amplify
  • Setting up an Amplify backend app
  • Adding a graphql api to our app
  • Configuring AWS IAM with appropriate permissions to access our api
  • Configuring our app to allow the IAM user to access the api
  • Building a signing function for requests made to the api from NodeJS

This should cover the entire process of setting up a graphql api within AppSync (using Amplify), to be consumed by an external NodeJS app via AWS_IAM auth.

Prerequisites

It is assumed here that you already have an AWS account set up, if that is not the case, you will need to head over here to set one up:

To get started you’ll need to head on over to this doc from AWS and set up your local machine to build and deploy an amplify backend. Note that while some of this setup can be done from the Amplify Studio, the config to setup IAM permissions must be done from the CLI.

Getting started: https://docs.amplify.aws/lib/project-setup/prereq/q/platform/js/

Setting up the Backend App

Once you’re set up we’ll create a new app, essentially following the "Initialize a new backend" section of this guide:

  mkdir amplify-api
  cd amplify-api

  amplify init

Setting up the API

Next we’ll add an api to our app and configure it accordingly, so jumping back in our :

  amplify add api
  • When prompted to make configuration selections you’ll want to select "Graphql", although in theory this guide should also work perfectly fine with REST as well.

  • Next you need to edit the "Authorization modes" option. By default this will be set to "API key" however we need to change this to "IAM", don’t worry about setting up any other auth types for now.

  • You’ll then choose "Blank Schema" and select "Yes" to edit that schema. This will open the "schema.graphql" file in your pre-defined editor, which we’ll add some code to so that it looks like this:

# This "input" configures a global authorization rule to enable public access to
# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

type Todo @model @auth(rules: [{ allow: private, provider: iam, operations: [read, create, update] }]) {
  id: ID!
  title: String!
  status: Boolean
}

You can configure this schema to look however you like, the main thing is for every model you wish to be able to access externally from the API, you need to ensure the "@auth" rules match the ones above, updating any operations you wish to include / omit.

We now need to deploy our API so run:

  amplify push

When greeted with " Are you sure you want to continue?" select "yes" and as for the other prompts that will follow this, you can just select "no".

This will do a number of things on the AWS end including creating a database in DynamoDB, setting up with the tables corresponding to our schema we set up before, and assigning permissions to all of these resources.

Setting up our IAM user

In your browser head over to your AWS account and navigate to the IAM section, here we’ll add a new user "amplify-api-user" – or whatever you like.

Hit next and then from the 3 options, select "Attach policies directly", and then click on "Create Policy" in the top right.

A new window should be opened and you can now create a security policy for this user restricting their access to only the AWS resources they need. In our case this policy will look like this (using the JSON editor):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "appsync:GraphQL"
            ],
            "Resource": "{APPSYNC_API_ARN}/*"
        }
    ]
}

To access your {APPSYNC_API_ARN} you’ll need to open a new window and head over to AppSync within AWS. Select the api we created earlier "amplify-api" and then on the left select "settings". You should see a field titled "API ARN", copy this value and insert it in the policy above. It should look something like this:

  arn:aws:appsync:ap-southeast-2:xxxxxxxxxxxxxx:apis/xxxxxxxxxxxxxxxxx

Granting IAM User permissions in our app

Now comes a critical step in being able to access our API via our IAM user. In our amplify-api folder on our local machine we should have a folder structure that looks similar to this:

- amplify/
 | - backend/
   | - api/
     | - amplifyapi/
       | - build/
       | - schema.graphql
       | - ...
   | - types/
   | - backend-config.json
   | - ...
 | - hooks/
 | cli.json
 | ...
- src/
 | - aws-exports.js

In the directory "/amplify-api/amplify/backend/api/amplifyapi/" we need to add a new file called "custom-roles.json":

  {
    "adminRoleNames": ["{AWS_IAM_ARN}"]
  }

Like with the "APPSYNC_API_ARN" before, we need to grab the ARN id for our IAM user. So in your browser navigate to the users section of IAM, select your user and copy the ARN value which should look something like this:

  arn:aws:iam::xxxxxxxxxxx:user/amplify-api-user

Once this file has been added to our app we can once again push these changes:

  amplify push

Creating access keys for our IAM user

The final step in setting up the AWS side of things is to add access keys to our IAM user. We’ll use these as the primary auth tokens when we make our requests later on.

Head over to the IAM section of AWS and select your user once again. Click on "Security credentials" and then scroll down to "Create access key". From the "Use cases", it doesn’t matter what you select, but we’ll just use "Third-party service". Add a dec if you like and you’ll then be taken to a screen with your "Access key" and "Secret access key".

Store these in a safe place, or even download the .csv file then we’re done.

Creating the request function

Finally we can now test out all that hard work, well, almost. We need to provide a way to sign requests made to our api using the access keys we just set up. This will be the code we set up on our external app running Nodejs to make the calls to our API.

The snippet below is a modified version of code found in these resources:

Without which this guide would not be possible so huge thanks to those authors!

First off you’ll need to install a couple of packages, these libraries are used to create the authed object we will send in our fetch request to AWS:

  npm i @smithy/signature-v4 @smithy/protocol-http @aws-crypto/sha256-js

Once those are installed we can import them and begin building our request object:

import { SignatureV4 } from '@smithy/signature-v4'
import { HttpRequest } from '@smithy/protocol-http'
import { Sha256 } from '@aws-crypto/sha256-js'

const {
  API_URL,
  AWS_ACCESS_KEY_ID,
  AWS_SECRET_ACCESS_KEY
} = process.env;

const apiUrl = new URL(API_URL!)

const signer = new SignatureV4({
  service: 'appsync',
  region: 'ap-southeast-2',
  credentials: {
    accessKeyId: AWS_ACCESS_KEY_ID!,
    secretAccessKey: AWS_SECRET_ACCESS_KEY!
  },
  sha256: Sha256,
})

export const signedFetch = async (graphqlObject) => {

  if (!graphqlObject) return

  // set up the HTTP request
  const request = new HttpRequest({
    hostname: apiUrl.host,
    path: apiUrl.pathname,
    body: JSON.stringify(graphqlObject),
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      host: apiUrl.hostname
    },
  })

  const signedRequest = await signer.sign(request)

  const { headers, body, method } = await signedRequest

  const awsSignedRequest = await fetch(API_URL!, {
    headers,
    body,
    method
  }).then((res) => res.json())

  return awsSignedRequest
}

The variables being called from "process.env" pertain to the "access key" and "secret access key" we created earlier, the "API_URL" refers to our Graphql API url which we can grab from our API in Appsync under the "GraphQL endpoint" in settings.

Then to use this function to make a request to the Graphql API:

const MyGraphqlQuery =  {
  query: `
    query getTodos {
      listTodos {
        items {
          title
          status
        }
      }
    }
  `
}


const response = signedRequest(MyGraphqlQuery).then((res) => res)

This can all be wrapped up in a set of functions/files or split out however best suites your app structure.


0

The above answer by Jezza is pretty good and it worked perfect for me. However I stumbled upon 2 other errors using this solution.

1. The browser is throwing a warning: Refused to set unsafe header "host"

I fixed that removing the header before doing the request with delete headers["host"]

...
const signedRequest = signer.sign(signingRequest);
const { headers, body, method } = await signedRequest;
// otherwise browser says 'Refused to set unsafe header "host"'
delete headers["host"];
const awsSignedRequest = await fetch(API_URL!, {
  headers,
  body,
  method
}).then((res) => res.json())
...

2. signing a request with Query params did not work for me.

The following solution fixed that:

// set up the HTTP request
  const request = new HttpRequest({
    hostname: apiUrl.host,
    path: apiUrl.pathname,
    body: JSON.stringify(graphqlObject),
    query: Object.fromEntries(apiUrl.searchParams),
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      host: apiUrl.hostname
    },
  })

  const signedRequest = await signer.sign(request)

New contributor

Tim Gutsche is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.



Leave a Reply

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