Skip to content

Lambda Authorizer Annotations Support#2284

Draft
GarrettBeatty wants to merge 1 commit intodevfrom
auth
Draft

Lambda Authorizer Annotations Support#2284
GarrettBeatty wants to merge 1 commit 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 +173 to +179
// Check for authorizer attributes on this Lambda function
var authorizerModel = ExtractAuthorizerModel(lambdaMethodSymbol, lambdaFunctionModel.ResourceName);
if (authorizerModel != null)
{
annotationReport.Authorizers.Add(authorizerModel);
}

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.

The PR introduces ValidateAuthorizerModel / ValidateAuthorizerReferences, but they are never invoked. As a result: (1) missing/invalid authorizer config won't produce diagnostics, and (2) duplicates or bad references can reach CloudFormationWriter and crash (e.g., via ToDictionary). Call these validations after collecting annotationReport.Authorizers / annotationReport.LambdaFunctions and treat failures as fatal for template sync.

Copilot uses AI. Check for mistakes.
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.
Comment on lines +1429 to +1498
"MyHttpAuthorizerAuthorizer": {
"Type": "AWS::ApiGatewayV2::Authorizer",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"ApiId": {
"Ref": "ServerlessHttpApi"
},
"AuthorizerType": "REQUEST",
"AuthorizerUri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiAuthorizer.Arn}/invocations"
},
"AuthorizerPayloadFormatVersion": "2.0",
"EnableSimpleResponses": true,
"IdentitySource": [
"$request.header.Authorization"
],
"Name": "MyHttpAuthorizer"
}
},
"HttpApiAuthorizerAuthorizerPermission": {
"Type": "AWS::Lambda::Permission",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"FunctionName": {
"Ref": "HttpApiAuthorizer"
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/authorizers/*"
}
}
},
"MyRestAuthorizerAuthorizer": {
"Type": "AWS::ApiGateway::Authorizer",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"Type": "TOKEN",
"AuthorizerUri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiAuthorizer.Arn}/invocations"
},
"IdentitySource": "method.request.header.Authorization",
"Name": "MyRestAuthorizer"
}
},
"RestApiAuthorizerAuthorizerPermission": {
"Type": "AWS::Lambda::Permission",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"FunctionName": {
"Ref": "RestApiAuthorizer"
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/authorizers/*"
}
}
},
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 baseline adds standalone AWS::ApiGatewayV2::Authorizer / AWS::ApiGateway::Authorizer resources and AWS::Lambda::Permission resources, but the updated CloudFormationWriter removes legacy standalone authorizer resources and writes authorizers inline under Resources.ServerlessHttpApi/ServerlessRestApi. Align this template with the new inline authorizer generation strategy to keep tests consistent.

Suggested change
"MyHttpAuthorizerAuthorizer": {
"Type": "AWS::ApiGatewayV2::Authorizer",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"ApiId": {
"Ref": "ServerlessHttpApi"
},
"AuthorizerType": "REQUEST",
"AuthorizerUri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiAuthorizer.Arn}/invocations"
},
"AuthorizerPayloadFormatVersion": "2.0",
"EnableSimpleResponses": true,
"IdentitySource": [
"$request.header.Authorization"
],
"Name": "MyHttpAuthorizer"
}
},
"HttpApiAuthorizerAuthorizerPermission": {
"Type": "AWS::Lambda::Permission",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"FunctionName": {
"Ref": "HttpApiAuthorizer"
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/authorizers/*"
}
}
},
"MyRestAuthorizerAuthorizer": {
"Type": "AWS::ApiGateway::Authorizer",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"Type": "TOKEN",
"AuthorizerUri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiAuthorizer.Arn}/invocations"
},
"IdentitySource": "method.request.header.Authorization",
"Name": "MyRestAuthorizer"
}
},
"RestApiAuthorizerAuthorizerPermission": {
"Type": "AWS::Lambda::Permission",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations"
},
"Properties": {
"FunctionName": {
"Ref": "RestApiAuthorizer"
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/authorizers/*"
}
}
},

Copilot uses AI. Check for mistakes.
ProcessTemplateDescription(report);

// Build authorizer lookup for processing events with Auth configuration
var authorizerLookup = report.Authorizers.ToDictionary(a => a.Name, a => a);
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.

Building authorizerLookup with report.Authorizers.ToDictionary(a => a.Name, ...) will throw if there are duplicate names (including a valid case of an HTTP API authorizer and a REST API authorizer sharing the same name). Consider keying by (AuthorizerType, Name) or building two lookups by type, and run the duplicate-name diagnostics before constructing any dictionary to avoid generator crashes.

Suggested change
var authorizerLookup = report.Authorizers.ToDictionary(a => a.Name, a => a);
var authorizerLookup = new Dictionary<string, Authorizer>(StringComparer.Ordinal);
foreach (var authorizer in report.Authorizers)
{
// Use TryAdd to avoid exceptions when multiple authorizers share the same name.
// The first authorizer encountered for a given name is preserved.
authorizerLookup.TryAdd(authorizer.Name, authorizer);
}

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.
else
{
_templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "REQUEST");
}
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.

REST API authorizers expose ResultTtlInSeconds on RestApiAuthorizerAttribute, but ProcessRestApiAuthorizers never persists it to the template. This makes the attribute property ineffective. Map it to the SAM authorizer TTL field (and validate/apply only when > 0 and within bounds).

Suggested change
}
}
// AuthorizerResultTtlInSeconds - cache TTL for authorizer result (only when valid)
// API Gateway REST Lambda authorizer TTL must be between 1 and 3600 seconds.
var resultTtlInSeconds = authorizer.ResultTtlInSeconds;
const int minTtlInSeconds = 1;
const int maxTtlInSeconds = 3600;
if (resultTtlInSeconds.HasValue &&
resultTtlInSeconds.Value >= minTtlInSeconds &&
resultTtlInSeconds.Value <= maxTtlInSeconds)
{
_templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", resultTtlInSeconds.Value);
}

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.
Comment on lines +350 to +353
}

_templateWriter.SetToken($"{restApiResourcePath}.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.

Similarly, ProcessRestApiAuthorizers unconditionally sets Resources.ServerlessRestApi.Metadata.Tool. If a project already defines ServerlessRestApi manually, this can cause the generator to treat it as managed and delete/modify nested Auth.Authorizers on subsequent runs. Gate this assignment the same way Lambda function resources are gated (only when absent or already created by this tool).

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

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.

1 participant