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
403Forbidden 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
AmazonAPIGatewayInvokeFullAccessmanaged 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.
- AWS SDK for .NET – AWS4Signer.cs
- AWS SDK for C++ – AWSAuthV4Signer.cpp
- AWS SDK for Go – v4.go
- AWS SDK for Java – BaseAws4Signer.java
- AWS SDK for JavaScript – v4.js
- AWS SDK for PHP – SignatureV4.php
- AWS SDK for Python (Boto) – signers.py
- AWS SDK for Ruby – signer.rb
Example of signing request with AWS v4 in Go
- Initialize a go project with
go mod init callapi - Install AWS SDK for Go
- Add the code in
main.go - 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
- Create a JavaScript project with
pnpm init - Install
aws4fetchwithpnpm i aws4fetch - Add the code for calling API in
index.mjs - Run code with
node index.mjs
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
- Add a route with
/{proxy+}this will take the priority over the default route - Add only methods for
OPTIONS - Set the authorizer to
NONE - 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.

Caution
aws4fetch doesn’t work with 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.