Authenticating with AWS IAM in AWS API Gateway
Posted by Kyle Pericak in cloud
This post is linked to from the AWS: Deep Dive Project
In this post we allow registered IAM users with a given policy access to an API Gateway endpoint. The API Gateway is front-ending another Lambda function with privileged access.
Build an API Gateway resource
Build a Lambda function for it to run
Open the lambda console and create a function. Here's an example python function that says hello to whoever sent the authenticated request.
def lambda_handler(event, context):
""" run a hello function """
body = 'NO HTTP METHOD'
if 'httpMethod' in event and event['httpMethod'] == 'POST':
request_body = event['body']
request_user = event['requestContext']['identity']['userArn']
body = f'{request_user} sent a POST with body: {request_body}'
elif 'httpMethod' in event and event['httpMethod'] == 'GET':
request_user = event['requestContext']['identity']['userArn']
body = f'{request_user} sent a GET'
return {
'statusCode': 200,
'body': json.dumps(body)
}
Define the API
- Go to the API Gateway Console
- Create
- Protocol: REST
- New API
- API Name:
<name>
- Endpoint type: Regional
- Create API
Create a method:
- Actions
- Create Method
- POST
- Check mark
- Integration Type: Lambda Function
- Use Lambda Proxy Integration: Checked
- Lambda Region: ca-central-1
- Lambda Function: HelloWorld
Enable IAM Authorization
- Click Method Request
- Set authorization to
AWS_IAM
IAM Setup
Create Policy
Collect the ARN of the API Gateway. Click your endpoint, then the method. The ARN will display under Method Request in the Method Execution diagram.
Navigate to the IAM console and create a new policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:Invoke"
],
"Resource": [
"arn:aws:execute-api:ca-central-1:850047500507:42mydti3lg/*/POST/*"
]
}
]
}
Bind policy to a user
Here's how I do it:
- Open the IAM console
- If you don't have a group yet, make one
- Attach the policy to the group
- Attach the group to a user
Collect the key and key ID of that user.
Test the API in Postman
Postman is nice since you know it works, and it natively handles building the AWS request headers. It's the easiest way I know to run a one-off test of your API.
Install Postman.
- New > Request - fill in some names.
- Set the request type to POST
- Enter the URL
- Enter any request data
- Go to the Authorization tab and choose AWS signature
- Enter the key and key ID from your IAM user
- Set the region, in my case ca-central-1
- Send
You should get a valid response from your API. If you skip any of the authorization steps you'll get an error instead.
Execute the API from Python
End users don't use postman. Usually they'd be using JavaScript, and in my case they use Python to access my API. This was really complicated.
Here's an open source library that can do it.
I've written my own version of this for the Breqwatr deployment tool. I didn't want to depend on the above link. Also I tried to rewrite the client as functions with the hope of making it easier to read.
You can find it in Breqwatr's GitHub
under lib/aws
.
Here's the high level flow of how it gets the auth header:
def get_authorization_header(time, key_id, secret_key, region, method, host,
body, uri, query):
""" Return an aws authorization header """
# canonical request is a multi-line string with a particular format
canonical_headers = get_canonical_headers(host, time)
payload_hash = get_payload_hash(body)
canonical_request = get_canonical_request(
method=method,
uri=uri,
query=query,
canonical_headers=canonical_headers,
payload_hash=payload_hash)
# create a string-to-sign from the canonical requests' digest
cr_digest = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
credential_scope = get_credential_scope(time, region)
string_to_sign = get_string_to_sign(time, credential_scope, cr_digest)
# sign the String-To-Sign with a signing key derived from secret iam key
signing_key = get_signing_key(secret_key, time, region)
signature = get_signature(signing_key, string_to_sign)
# return the headers all in one string
return (
f'AWS4-HMAC-SHA256 Credential={key_id}/{credential_scope}, '
f'SignedHeaders=host;x-amz-date, '
f'Signature={signature}')