Search code examples
node.jsgraphqlamazon-iamaws-amplifyaws-appsync

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


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.


Solution

  • 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.