Signing calls to Lambda Function URLs using IAM auth with HttpClient
Want to learn more about AWS Lambda and .NET? Check out my A Cloud Guru course on ASP.NET Web API and Lambda.
Download full source code.
I’ve written a few posts about Lambda Function URLs, in those I showed how to create a Function URL with Auth type NONE and call it.
In this post, you will see how to use create a Function URL that uses IAM to authenticate requests, and how to call it using the .NET HttpClient
class, with the help of a library from Mattias Kindborg. This library adds an AWS Signature Version 4 to the HTTP requests.
Getting the tools
Install the latest tooling, this lets you deploy and run Lambda functions.
dotnet tool install -g Amazon.Lambda.Tools
Install the latest templates to get .NET 6 support.
dotnet new --install Amazon.Lambda.Templates
Get the latest version of the AWS CLI, from here.
The Lambda Function with the function URL
There are a few steps to create a Lambda Function and add the function URL, but they are easy to follow.
Create a simple application for the Lambda function
This template does not support Function URLs out of the box, but a few minor changes is all that’s needed.
From the command line, run -
dotnet new lambda.EmptyFunction --name FunctionUrlExample_IAMAuth
Go to the directory .\FunctionUrlExample_IAMAuth\src\FunctionUrlExample_IAMAuth\
.
The FunctionHandler
method is very simple -
public string FunctionHandler(string input, ILambdaContext context)
{
return input.ToUpper();
}
You will replace this in the next step.
Altering the Lambda function to accept the Function URL request
When the Lambda function is triggered from the Function URL the incoming request will contain all the HTTP info you would expect, query-string, body, headers, method, etc, this will be passed to the Lambda function as JSON. To accept this, you will change the first parameter in the FunctionHandler
method from a string
to an APIGatewayHttpApiV2ProxyRequest
.
But first, add the Amazon.Lambda.APIGatewayEvents
package to the project -
dotnet add package Amazon.Lambda.APIGatewayEvents
Add a using statement to the Function.cs
file -
using Amazon.Lambda.APIGatewayEvents;
Change the FunctionHandler
signature to -
public string FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
With this change, the incoming request will be deserialized into the request
object.
If you are responding to a GET
or another verb that doesn’t use a body (I know, GET
technically can have a body, but it usually doesn’t), you can extract the query string, path, etc from the request
object. For example, try -
request.QueryStringParameters
request.PathParameters
request.RequestContext.Http.Method
But if you want to use the body of the request, another deserialization must be performed.
Deserializing the body of the request
The APIGatewayHttpApiV2ProxyRequest.Body
contains the body of any request that supports a body (PUT, POST, PATCH, etc). But this is a string and has to be explicitly deserialized into a type you define.
In this example, you will deserialize a Person
from the body of the request.
Create a new class called Person
.
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public int Age { get; init; }
}
Back in Function.cs
, add two using statements -
using System.Text.Json;
using System.Text.Json.Serialization;
And change the FunctionHandler
method to this -
public string FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
var serializationOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling= JsonNumberHandling.AllowReadingFromString
};
Person person = JsonSerializer.Deserialize<Person>(request.Body, serializationOptions);
return $"Hello {person.FirstName} {person.LastName}, you are {person.Age} years old.";
}
Setting serializationOptions
is not necessary but makes it easier for your function to handle capitalization issues and the presence of numbers as strings in the request.
Deploy the function
Use the following to build the code and deploy the Lambda function -
dotnet lambda deploy-function FunctionUrlExample_IAMAuth
You will then be asked - “Enter name of the new IAM Role:”, put in “FunctionUrlExample_IAMAuthRole”.
Then you will be asked to - “Select IAM Policy to attach to the new role and grant permissions”, select “AWSLambdaBasicExecutionRole”, for me it is number 6 on the list.
Wait as the function and permissions are created.
Configuring the function for HTTP requests
Right now, the Lambda function is deployed and can be seen in the AWS Console, but it doesn’t have a Function URL.
You can add one from the AWS Console or the AWS CLI (or other tools). But I want to use the AWS CLI, a minimum version of “2.5.4” is required (see above on tooling).
From the command line, run -
aws lambda create-function-url-config --function-name FunctionUrlExample_IAMAuth --auth-type AWS_IAM
You will see -
{
"FunctionUrl": "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/",
"FunctionArn": "arn:aws:lambda:us-east-1:xxxxxxxxxxx:function:FunctionUrlExample_IAMAuth",
"AuthType": "NONE",
"CreationTime": "2022-04-09T00:43:45.699805Z"
}
At this point, a URL is attached to the Lambda function, requiring IAM authentication.
If you try to open the URL from Fiddler, Postman, etc, you will get a 403 response and {"Message":"Forbidden"}
in the body.
Using HttpClient to call the Lambda Function URL
Now that the Lambda function has a Function URL and it requires IAM authentication, you have to make a properly signed request to the endpoint.
With the AwsSignatureVersion4 package, this is very easy.
Create a new .NET console application.
dotnet new console -n FunctionUrlExample_IAMAuthCaller
Add two NuGet packages -
dotnet add package AWSSDK.Core
dotnet add package AwsSignatureVersion4
Replace the code in Program.cs with this -
using System.Text;
using System.Text.Json;
using Amazon.Runtime;
var credentials = FallbackCredentialsFactory.GetCredentials();
const string FunctionUrl = "https://3qkrmovyukgnewrmc2evsebykq0grpbh.lambda-url.us-east-1.on.aws/";
const string Region = "us-east-1";
var httpClient = new HttpClient();
// using httpClient.PostAsync(..) to send the request
var person1 = new Person { FirstName = "Alan", LastName = "Adams", Age = 11 };
HttpContent httpContent = new StringContent(JsonSerializer.Serialize(person1), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(
FunctionUrl,
httpContent,
regionName: Region,
serviceName: "lambda",
credentials: credentials);
Console.WriteLine(await response.Content.ReadAsStringAsync());
// using httpClient.SendAsync(..) to send the request
var person2 = new Person { FirstName = "Beth", LastName = "Bates", Age = 22 };
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, FunctionUrl);
httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(person2), Encoding.UTF8, "application/json");
var response2 = await httpClient.SendAsync(
httpRequestMessage,
regionName: Region,
serviceName: "lambda",
credentials: credentials);
Console.WriteLine(await response2.Content.ReadAsStringAsync());
/*----------------------------------------------*/
public class Person
{
public string? FirstName { get; init; }
public string? LastName { get; init; }
public int Age { get; init; }
}
You should now be able to call the Lambda function because the required signature will be added to the request.
Try running the code.
You will see the following output -
Hello Alan Adams, you are 11 years old.
Hello Beth Bates, you are 22 years old.
Big thank you to Mattias Kindborg for this very easy to use library.
Download full source code.