2 Answers
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:
- https://dev.to/zirkelc/sign-graphql-request-with-aws-iam-and-signature-v4-2il6
- https://docs.amplify.aws/lib/graphqlapi/graphql-from-nodejs/q/platform/js/#iam-authorization
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.
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)