Signing calls to Lambda Function URLs using IAM auth with HttpClient

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 APIGatewayProxyRequest.

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(APIGatewayProxyRequest 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 APIGatewayProxyRequest.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(APIGatewayProxyRequest 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 be asked - “Select IAM Role that to provide AWS credentials to your code:”, select “*** Create new IAM Role ***”

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.

comments powered by Disqus

Related