How to Build & Use AWS API Gateway with IAM Authorizer

Updated on
API Gateway IAM auth banner

Amazon API Gateway is a serverless API routing service which helps developers create, publish and manage APIs, be it HTTP, REST, or WebSocket.

You might ask: Why to even add authorization in your APIs?

Public APIs are accessible by anyone, including hackers. You don’t want to get DDoS attacks on your API and Lambda.

If you want to provide role-based access in your APIs, e.g. admins, editors, viewers etc., you can simply create an IAM user with permission to call only APIs which users are authorized to.

The easiest way to add authorization in your API Gateway is to add IAM Authorization in your routes.

After going through this article, you will be able to build and integrate HTTP / REST APIs with IAM authorizer.

Create HTTP API with CDK with IAM auth

Let’s create a CDK project with npx

Feel free to use pnpx or bunx

npx cdk init app --language typescript

Tip

Don’t know how to use AWS CDK?

Follow this step-by-step article to create a TypeScript CDK project

Define API Gateway V2 Stack

Create a NodeJS Lambda function and HTTP API with default integration and default authorizer:

import * as cdk from "aws-cdk-lib";
import {
  CorsHttpMethod,
  HttpApi,
  HttpMethod,
  HttpNoneAuthorizer,
} from "aws-cdk-lib/aws-apigatewayv2";
import { HttpIamAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class ApiGwIamAuthStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const apiHandlerFn = new NodejsFunction(this, "apigw-handler", {
      runtime: Runtime.NODEJS_20_X,
      entry: `${__dirname}/api-handler-fn.ts`,
    });

    const lambdaFnIntegration = new HttpLambdaIntegration(
      "api-handler",
      apiHandlerFn,
    );

    // The code that defines your stack goes here
    const api = new HttpApi(this, "only-auth-api", {
      defaultAuthorizer: new HttpIamAuthorizer(),
      defaultIntegration: lambdaFnIntegration,
    });

    new cdk.CfnOutput(this, "API_URL", {
      value: api.url!,
    });
  }
}

Lambda Function Code for API Handler

Since it’s just an example, we respond to what we receive with status: 200:

import { APIGatewayProxyHandlerV2 } from "aws-lambda";

export const handler: APIGatewayProxyHandlerV2 = async (e) => {
  return {
    statusCode: 200,
    headers: {
      "Content-Type": "application/json",
    },

    body: JSON.stringify(
      {
        method: e.requestContext.http.method,
        path: e.requestContext.http.path,
        body: e.body ? JSON.parse(e.body) : "",
      },
      null,
      4,
    ),
  };
};

Deploy API Gateway with CDK

npx cdk deploy

CDK output will look like this with the API URL:

❯ cdk deploy
Bundling asset ApiGwIamAuthStack/apigw-handler/Code/Stage...

  ...b25050fd80fa7c125314b89ecaace2754ceca155943ba763890ead819411852/index.mjs  873b

⚡ Done in 1ms

✨  Synthesis time: 3.31s

ApiGwIamAuthStack: deploying... [1/1]

 ✅  ApiGwIamAuthStack

✨  Deployment time: 2.47s

Outputs:
ApiGwIamAuthStack.APIURL = https://xxx.execute-api.us-east-1.amazonaws.com/
Stack ARN:
arn:aws:cloudformation:us-east-1:xxxxxxx:stack/ApiGwIamAuthStack/a68e0b90-df87-11ee-8f61-0e011f7ad47d

✨  Total time: 5.78s

Testing the API endpoint

Curious on what happens when you visit the URL?

You get a 403 Forbidden response

This happened because we’re not passing authentication header in our request.

Create IAM User with API Invoke Policy

We will need to have an IAM user which has permission to invoke the API.

Note

Skip this section if you already have an admin user and AWS access key and secret

IAM Policy Statement for Invoking API

Feel free to limit the resource to only the APIs you need to be allowed, instead of putting * to give permission to invoke any API in your AWS account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["execute-api:Invoke"],
      "Resource": "arn:aws:execute-api:*:*:*"
    }
  ]
}

How to create IAM user and get access keys:

  • Create an IAM user in IAM console
  • Attach AmazonAPIGatewayInvokeFullAccess managed policy on the user
  • Generate the access key id and secret access key from Security Credentials

Tip

Looking for a detailed guide on creating IAM user?

Here is a step by step guide on setting up IAM user and getting access keys

Authenticate Request with AWS Signature v4

To authenticate our requests, AWS uses sign v4 Authentication headers.

Here is what you need to know about AWS-4

AWS Sign v4 is generated using these:

  • Access key id
  • Secret access key
  • Current time
  • Service name
  • Service region
  • Payload / Body in SHA256

AWS Sign v4 sample

Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/ec2/aws4_request,
SignedHeaders=host;x-amz-date,
Signature=calculated-signature

Benefits of AWS Sign v4

You might ask, why are we not just sending our access key id and secret access key, instead of generating this big token by doing all the math?

Here are the reasons to use Sign v4:

  • Protects your secret key in transit — Hackers can’t see your secret key because its encrypted.
  • Expires after 5 minutes — Even if someone steals, they won’t be able to do anything by the time.

Set AWS environment variables

Note

Only needed if you want to secure and reuse your keys. Feel free to pass it directly whenever you want.

On your bash terminal / CLI, set the following variables:

export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=xxx+xxxx+xxx

cURL command for invoking IAM Auth API

How to call APIs with Sigv4 headers? With curl, it’s simple to call any AWS API with AWS Sign v4 Authentication header.

  • You pass the URL of your API
  • Service name and region in --aws-sigv4
  • Access key id and secret access key in --user
curl https://xxx.execute-api.us-east-1.amazonaws.com/test-me-bro --aws-sigv4 "aws:amz:us-east-1:execute-api" --user "$AWS_ACCESS_KEY_ID":"$AWS_SECRET_ACCESS_KEY"

API response from cURL command

You can see the method and path which was returned from the Lambda:

{
  "method": "GET",
  "path": "/test-me-bro",
  "body": ""
}

Send JSON from cURL

To send JSON data we can specify 'Content-Type: application/json' in the --header param and send the JSON string in --data param:

curl $API_URL
--aws-sigv4 "aws:amz:us-east-1:execute-api"
--user "$AWS_ACCESS_KEY_ID":"$AWS_SECRET_ACCESS_KEY"
--data '{"msg":"cURL you are the best!"}'
--header 'Content-Type: application/json'

Response is returned with POST method and the message we sent:

{
  "method": "POST",
  "path": "/",
  "body": {
    "msg": "cURL you are the best!"
  }
}

Invoke API with IAM Auth using AWS SDK

Here is the list of code in most popular languages.

Example of signing request with AWS v4 in Go

  1. Initialize a go project with go mod init callapi
  2. Install AWS SDK for Go
  3. Add the code in main.go
  4. Run the program with go run .
package main

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
	config "github.com/aws/aws-sdk-go-v2/config"
)

func main() {
	ctx := context.Background()
	signer := v4.NewSigner()

	// Load default AWS config from ENV or file
	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion("us-east-1"),
	)
	if err != nil {
		log.Fatalf("unable to load SDK config, %v", err)
	}

	// Retrieve credentials of AWS (Access Key, Secret)
	credentials, err := cfg.Credentials.Retrieve(context.TODO())

	// create JSON body
	body := map[string]interface{}{
		"message": "Hello from LearnAWS.io",
	}

	jsonBody, _ := json.Marshal(body)

	// Get API URL from env
	apiUrl := os.Getenv("API_URL")

	if apiUrl == "" {
		log.Fatal("API_URL not found in env")
	}

	httpReq, err := http.NewRequest("POST", apiUrl, bytes.NewReader(jsonBody))

	httpReq.Header.Add("Content-Type", "application/json")

	// Generate sha256 of json body
	payloadSha := sha256.Sum256([]byte(jsonBody))

	// Sign the HTTP request
	signer.SignHTTP(
		ctx,
		credentials,
		httpReq,
		// encode payload Sha256 into hex
		hex.EncodeToString(payloadSha[:]),
		"execute-api", "us-east-1", time.Now(),
	)

	client := &http.Client{}

	resp, err := client.Do(httpReq)
	bodyBytes, err := io.ReadAll(resp.Body)
	resp.Body.Close()

	fmt.Print(string(bodyBytes))

}

Output from your Go program will look like this:

{
  "method": "POST",
  "path": "/",
  "body": {
    "message": "Hello from LearnAWS.io"
  }
}

Invoke API Gateway with IAM Auth in NodeJS

  1. Create a JavaScript project with pnpm init
  2. Install aws4fetch with pnpm i aws4fetch
  3. Add the code for calling API in index.mjs
  4. Run code with node index.mjs

Caution

NodeJS v20+ is required to use aws4fetch.

Use aws4 for < NodeJS v20.

This code will work with Bun, Cloudflare Workers and Deno.

import { AwsClient } from "aws4fetch";

if (typeof crypto === "undefined") {
  throw Error("Subtle crypto is not defined, please use NodeJS v20+");
}

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

if (!API_URL) {
  throw Error("API_URL not found in env");
}

const aws = new AwsClient({
  accessKeyId: AWS_ACCESS_KEY_ID,
  secretAccessKey: AWS_SECRET_ACCESS_KEY,
  // Good to provide incase it fails to parse service and region from URL
  service: "execute-api",
  region: "us-east-1",
});

const res = await aws.fetch(API_URL, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ message: "Hello from LearnAWS.io" }),
});

console.log(await res.text());

Output from running NodeJS file:

{
  "method": "POST",
  "path": "/",
  "body": {
    "message": "Hello from LearnAWS.io"
  }
}

Calling API Gateway with IAM Auth from React Frontend

Create React app with Vite

pnpm create vite ReactApp --template react-swc-ts

Install aws4fetch

pnpm i aws4fetch

Update App.tsx code

By using HTML Form with two inputs and a textarea, we can get access key id, secret and the message we want to send.

Tip

Access key and secret can be encrypted and stored in localstorage as well to avoid putting it every time in the input.

import { AwsClient } from "aws4fetch";
import { FormEventHandler, useState } from "react";
const API_URL = import.meta.env.VITE_API_URL;
function App() {
  const [msg, setMsg] = useState("");

  const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
    e.preventDefault();
    // extract form data from the form
    const data = new FormData(e.currentTarget);
    const { id, secret, message } = Object.fromEntries(data) as {
      id: string;
      secret: string;
      message: string;
    };

    // create new AWS client
    const aws = new AwsClient({
      accessKeyId: id,
      secretAccessKey: secret,
      // Good to provide in case it fails to parse service and region from URL
      service: "execute-api",
      region: "us-east-1",
    });

    // use fetch from aws client instead of global fetch
    const res = await aws.fetch(API_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message }),
    });

    // update the message state
    setMsg(await res.text());
  };
  return (
    <>
      <form onSubmit={handleSubmit} className="form-container">
        <input name="id" required type="text" placeholder="Access Key ID" />
        <input
          name="secret"
          required
          type="password"
          placeholder="Secret Access Key"
        />
        <textarea
          name="message"
          required
          placeholder="Type your message"
          defaultValue="Hello from LearnAWS.io"
        ></textarea>
        <button type="submit">Submit</button>
      </form>
      <code>{msg}</code>
    </>
  );
}

export default App;

How to fix CORS with API Gateway

If you try to submit the request, you’ll get CORS error with status code 403 right away.

XHR OPTIONS [https://xxx.execute-api.us-east-1.amazonaws.com/]
CORS Missing Allow Origin

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at [https://xxx.execute-api.us-east-1.amazonaws.com/]
(Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 403.


Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.

This is happening because all our routes will have to have authorization header, but in preflight / OPTIONS request headers can’t be included.

Update API Gateway code in CDK

Step 1: Enable CORS preflight

We want to tell our browsers that the API is willing to accept the headers and these origins can send request to the server.

const api = new HttpApi(this, "humbleinvestor-blogs-api", {
  // add your CORS config
  corsPreflight: {
    // these headers are allowed to be sent
    allowHeaders: [
      "Authorization",
      "X-Amz-Date",
      "Content-Type",
      "x-requested-with",
    ],
    allowMethods: [CorsHttpMethod.ANY],
    // specify your domains in the array
    allowOrigins: ["http://localhost:5173"],
    maxAge: cdk.Duration.days(1),
  },
  // rest is same
  defaultAuthorizer: new HttpIamAuthorizer(),
  defaultIntegration: lambdaFnIntegration,
});
Step 2: Override OPTIONS Method
  1. Add a route with /{proxy+} this will take the priority over the default route
  2. Add only methods for OPTIONS
  3. Set the authorizer to NONE
  4. Use the default Lambda Function integration
api.addRoutes({
  path: "/{proxy+}",
  methods: [HttpMethod.OPTIONS],
  authorizer: new HttpNoneAuthorizer(),
  integration: lambdaFnIntegration,
});
Step 3: Update Lambda function to respond with 204

Inside your handler add a if check for OPTIONS in http.method and return status code 204 which means no content.

if (e.requestContext.http.method === "OPTIONS") {
  return {
    statusCode: 204,
  };
}
Step 4: Deploy CORS changes

Run the cdk deploy command and try sending the request again.

React UI Demo with Request & Response

In the screenshot below you can see that authorization header has been sent with the fetch request.

React web app screenshot with aws access key id and secret showing response from secure IAM API

Caution

aws4fetch doesn’t work with React Native

Read our blog on calling AWS API Gateway in React Native

I hope this guide helped you in your AWS journey. What are you waiting for? Go invoke some APIs.

Let me know what you are working on in the comments ;)

All the code used for demo and CDK can be found on the API Gateway IAM Auth GitHub repo.


Hills 🏔 and Skills, What's Common?

They both need you to be on top.

You will get lifetime access with:

All yours, just at:

$149

Just type and your search result will magically appear here