Skip to content

Lambda Authorizer Annotations Support#2284

Draft
GarrettBeatty wants to merge 3 commits intodevfrom
auth
Draft

Lambda Authorizer Annotations Support#2284
GarrettBeatty wants to merge 3 commits intodevfrom
auth

Conversation

@GarrettBeatty
Copy link
Contributor

@GarrettBeatty GarrettBeatty commented Feb 17, 2026

Pull Request: Lambda Authorizer Annotations Support

Description

This PR adds declarative Lambda Authorizer support to the AWS Lambda Annotations framework. Developers can now define Lambda Authorizers and protect API endpoints entirely through C# attributes, eliminating the need for manual CloudFormation configuration.

New Attributes

[HttpApiAuthorizer] - For HTTP API (API Gateway V2)

Property Description Default
Name Unique identifier referenced by endpoints via Authorizer = "Name" (required) -
IdentitySource Header used as the cache key when caching is enabled. Should match the header your code reads for authentication "Authorization"
PayloadFormatVersion Request/response format sent to your Lambda. "2.0" provides a simpler structure; "1.0" matches REST API format "2.0"
ResultTtlInSeconds Time in seconds API Gateway caches authorization results. 0 disables caching. Max 3600 0

[RestApiAuthorizer] - For REST API (API Gateway V1)

Property Description Default
Name Unique identifier referenced by endpoints via Authorizer = "Name" (required) -
IdentitySource Header to extract. For Token type, this value is passed directly in request.AuthorizationToken. Also used as cache key when caching is enabled "Authorization"
Type Token: API Gateway extracts the header and passes it in request.AuthorizationToken. Request: Full request context is passed Token
ResultTtlInSeconds Time in seconds API Gateway caches authorization results. 0 disables caching. Max 3600 0

Updated Attributes

  • [HttpApi] - Added Authorizer property to reference an authorizer by name
  • [RestApi] - Added Authorizer property to reference an authorizer by name

Basic Usage

HTTP API Authorizer Example

// Step 1: Define the authorizer
[LambdaFunction]
[HttpApiAuthorizer(Name = "MyHttpAuthorizer")]
public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(
    APIGatewayCustomAuthorizerV2Request request,
    ILambdaContext context)
{
    var token = request.Headers?.GetValueOrDefault("authorization", "");
    
    if (IsValidToken(token))
    {
        return new APIGatewayCustomAuthorizerV2SimpleResponse
        {
            IsAuthorized = true,
            Context = new Dictionary<string, object>
            {
                { "userId", "user-123" },
                { "email", "user@example.com" }
            }
        };
    }
    
    return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false };
}

// Step 2: Protect endpoints by referencing the authorizer name
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyHttpAuthorizer")]
public object GetProtectedResource(
    [FromCustomAuthorizer(Name = "userId")] string userId,
    [FromCustomAuthorizer(Name = "email")] string email)
{
    return new { UserId = userId, Email = email };
}

REST API Authorizer Example

// Define a Token-based REST API authorizer
// With Type = Token, API Gateway extracts the IdentitySource header value
// and passes it directly in request.AuthorizationToken
[LambdaFunction]
[RestApiAuthorizer(Name = "MyRestAuthorizer", Type = RestApiAuthorizerType.Token)]
public APIGatewayCustomAuthorizerResponse Authorize(
    APIGatewayCustomAuthorizerRequest request,
    ILambdaContext context)
{
    var token = request.AuthorizationToken; // Token extracted by API Gateway
    
    if (IsValidToken(token))
    {
        return new APIGatewayCustomAuthorizerResponse
        {
            PrincipalID = "user-123",
            PolicyDocument = new APIGatewayCustomAuthorizerPolicy
            {
                Version = "2012-10-17",
                Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>
                {
                    new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement
                    {
                        Effect = "Allow",
                        Action = new HashSet<string> { "execute-api:Invoke" },
                        Resource = new HashSet<string> { request.MethodArn }
                    }
                }
            }
        };
    }
    // Return deny policy for invalid tokens...
}

// Protect a REST API endpoint
[LambdaFunction]
[RestApi(LambdaHttpMethod.Get, "/api/secure", Authorizer = "MyRestAuthorizer")]
public object GetSecureResource([FromCustomAuthorizer(Name = "userId")] string userId)
{
    return new { UserId = userId };
}

Authorizer with Custom Header and Caching

// Use X-Api-Key header instead of Authorization
// IdentitySource must match the header you read in your code
// API Gateway uses this header's value as the cache key
[LambdaFunction]
[HttpApiAuthorizer(
    Name = "ApiKeyAuthorizer", 
    IdentitySource = "X-Api-Key",
    ResultTtlInSeconds = 300)]
public APIGatewayCustomAuthorizerV2SimpleResponse ValidateApiKey(
    APIGatewayCustomAuthorizerV2Request request,
    ILambdaContext context)
{
    var apiKey = request.Headers?.GetValueOrDefault("x-api-key", "");
    // Validate API key...
}

What Gets Generated

The source generator automatically creates all necessary CloudFormation resources:

  • Lambda function resources for authorizers
  • AWS::ApiGatewayV2::Authorizer or AWS::ApiGateway::Authorizer resources
  • AWS::Lambda::Permission resources for API Gateway to invoke authorizers
  • Proper Auth.Authorizer configuration on protected routes

@GarrettBeatty GarrettBeatty changed the title Auth Lambda Authorizer Annotations Support Feb 17, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds declarative Lambda Authorizer support to the Amazon.Lambda.Annotations framework by introducing new authorizer attributes and extending the source generator to emit corresponding SAM Auth configuration for protected API events.

Changes:

  • Introduces [HttpApiAuthorizer] / [RestApiAuthorizer] attributes plus supporting models/builders in the source generator.
  • Extends [HttpApi] / [RestApi] with an Authorizer property and updates CloudFormation template generation accordingly.
  • Updates/introduces test applications and baseline templates to exercise authorizer scenarios.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs New attribute surface for HTTP API Lambda authorizers.
Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs New attribute surface for REST API Lambda authorizers.
Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs Adds Authorizer property for protecting HTTP API routes.
Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs Adds Authorizer property for protecting REST API routes.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs Detects authorizer attributes and adds authorizer data into the report.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs Writes authorizer definitions and route-level Auth config into templates; adds orphan cleanup.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/* Adds authorizer models and wiring through report + lambda function model.
Libraries/test/TestServerlessApp/AuthorizerFunctions.cs New test functions demonstrating protected/public endpoints and authorizers.
Libraries/test/TestServerlessApp/serverless.template Updated expected baseline template for the new authorizer scenarios.
Libraries/test/TestCustomAuthorizerApp/* Updates sample app + template to use new Authorizer property.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs Adjusts test model to include new Authorizer property.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1203 to 1231
"SyncedEventProperties": {
"RootGet": [
"Path",
"Method",
"Auth.Authorizer.Ref"
]
}
},
"Properties": {
"Runtime": "dotnet6",
"CodeUri": ".",
"MemorySize": 512,
"Timeout": 30,
"Policies": [
"AWSLambdaBasicExecutionRole"
],
"PackageType": "Zip",
"Handler": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_GetProtectedResource_Generated::GetProtectedResource",
"Events": {
"RootGet": {
"Type": "HttpApi",
"Properties": {
"Path": "/api/protected",
"Method": "GET",
"Auth": {
"Authorizer": {
"Ref": "MyHttpAuthorizerAuthorizer"
}
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This expected template uses Auth.Authorizer.Ref for the new authorizer-protected HttpApi events, but CloudFormationWriter now emits Auth.Authorizer as a string authorizer name (inline SAM authorizers). Update this baseline to match the generator output, otherwise the snapshot/template assertions will fail.

Copilot uses AI. Check for mistakes.
// AuthorizerResultTtlInSeconds (only if caching is enabled)
if (authorizer.ResultTtlInSeconds > 0)
{
_templateWriter.SetToken($"{authorizerPath}.FunctionInvokeRole", null); // Required for caching
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResultTtlInSeconds is never written to the SAM authorizer configuration. The code currently sets FunctionInvokeRole to null when TTL > 0, which both ignores the TTL value and introduces an unexpected property. Instead, emit AuthorizerResultTtlInSeconds (and only when > 0), and avoid writing unrelated/null tokens.

Suggested change
_templateWriter.SetToken($"{authorizerPath}.FunctionInvokeRole", null); // Required for caching
_templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", authorizer.ResultTtlInSeconds);

Copilot uses AI. Check for mistakes.
Comment on lines 307 to 309
}

_templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessHttpApiAuthorizers stamps Resources.ServerlessHttpApi.Metadata.Tool = "Amazon.Lambda.Annotations" even when the resource already exists. This can inadvertently mark a user-managed ServerlessHttpApi as generator-managed and allow later cleanup logic to remove user-defined auth config. Consider only setting Metadata.Tool when creating the resource, or only if it is already marked as created by this tool.

Suggested change
}
_templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL);
_templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL);
}

Copilot uses AI. Check for mistakes.
@GarrettBeatty GarrettBeatty force-pushed the auth branch 4 times, most recently from aead1d4 to 663a5b0 Compare February 25, 2026 17:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 67 to 71
var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi");
var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi");
Console.WriteLine($"[IntegrationTest] ServerlessHttpApi: {httpApiId}, AnnotationsRestApi: {restApiId}");
HttpApiUrlPrefix = $"https://{httpApiId}.execute-api.{region}.amazonaws.com";
RestApiUrlPrefix = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod";
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi") will always return null for TestServerlessApp because the generated template doesn't contain an AnnotationsRestApi logical resource (it uses SAM's implicit ServerlessRestApi when you have Type: Api events). This makes RestApiUrlPrefix invalid. Use the correct logical ID (likely ServerlessRestApi), or explicitly define the REST API resource in the template if that's the new intent.

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 79
LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName);
Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}");

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LambdaFunctions can contain entries with Name == null (you already handle that in the log message). Later in initialization this list is used to wait for functions to become active; ensure null names are filtered out before invoking Lambda APIs, otherwise GetFunctionConfigurationAsync will throw when passed a null function name.

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 73
dotnet restore
Write-Host "Creating CloudFormation Stack $identifier, Architecture $arch, Runtime $runtime"
dotnet lambda deploy-serverless --template-parameters "ArchitectureTypeParameter=$arch"
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotnet lambda deploy-serverless --template-parameters "ArchitectureTypeParameter=$arch" still passes ArchitectureTypeParameter, but TestServerlessApp/serverless.template no longer defines a Parameters.ArchitectureTypeParameter. CloudFormation deployments will fail with an unknown parameter. Either reintroduce the parameter in the template or remove the --template-parameters argument (and rely on whatever mechanism is now used to set architecture).

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +149
private static string ConvertSqsUrlToArn(string queueUrl)
{
// SQS URL format: https://sqs.{region}.amazonaws.com/{account-id}/{queue-name}
var uri = new Uri(queueUrl);
var host = uri.Host; // sqs.us-west-2.amazonaws.com
var segments = uri.AbsolutePath.Trim('/').Split('/'); // [account-id, queue-name]
var region = host.Split('.')[1]; // us-west-2
var accountId = segments[0];
var queueName = segments[1];
return $"arn:aws:sqs:{region}:{accountId}:{queueName}";
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConvertSqsUrlToArn assumes queueUrl is non-null and has exactly two path segments after trimming. If GetResourcePhysicalIdAsync returns null or an unexpected URL format, this will throw (e.g., new Uri(queueUrl) or segments[1]). Add explicit null/format checks and a clearer failure message (or assert the resource ID is not null before calling this helper).

Copilot uses AI. Check for mistakes.
var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsHttpApi");
var implicitHttpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi");
var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi");
Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}");
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetResourcePhysicalIdAsync can return null when the logical resource ID isn't present or the stack query fails. Here, httpApiId/implicitHttpApiId/restApiId are interpolated directly into URLs, which can produce invalid URLs like https://.execute-api... and make failures harder to diagnose. Add explicit asserts (or throw) when any of these IDs are null/empty before constructing the base URLs.

Suggested change
Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}");
Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}");
Assert.False(string.IsNullOrEmpty(httpApiId), $"CloudFormation resource 'AnnotationsHttpApi' was not found or has an empty physical ID for stack '{_stackName}'.");
Assert.False(string.IsNullOrEmpty(implicitHttpApiId), $"CloudFormation resource 'ServerlessHttpApi' was not found or has an empty physical ID for stack '{_stackName}'.");
Assert.False(string.IsNullOrEmpty(restApiId), $"CloudFormation resource 'AnnotationsRestApi' was not found or has an empty physical ID for stack '{_stackName}'.");

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants