Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Libraries/Amazon.Lambda.Annotations.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
"src\\Amazon.Lambda.Serialization.SystemTextJson\\Amazon.Lambda.Serialization.SystemTextJson.csproj",
"test\\Amazon.Lambda.Annotations.SourceGenerators.Tests\\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj",
"test\\TestExecutableServerlessApp\\TestExecutableServerlessApp.csproj",
"test\\IntegrationTests.Helpers\\IntegrationTests.Helpers.csproj",
"test\\TestCustomAuthorizerApp\\TestCustomAuthorizerApp.csproj",
"test\\TestCustomAuthorizerApp.IntegrationTests\\TestCustomAuthorizerApp.IntegrationTests.csproj",
"test\\TestServerlessApp.IntegrationTests\\TestServerlessApp.IntegrationTests.csproj",
"test\\TestServerlessApp.NET8\\TestServerlessApp.NET8.csproj",
"test\\TestServerlessApp\\TestServerlessApp.csproj"
]
}
}
}
565 changes: 555 additions & 10 deletions Libraries/Libraries.sln

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if(att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.FromCustomAuthorizerAttribute), SymbolEqualityComparer.Default))
{
var data = FromCustomAuthorizerAttributeBuilder.Build(att);
model = new AttributeModel<FromCustomAuthorizerAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAttribute), SymbolEqualityComparer.Default))
{
var data = HttpApiAttributeBuilder.Build(att);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Amazon.Lambda.Annotations.APIGateway;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
public class FromCustomAuthorizerAttributeBuilder
{
public static FromCustomAuthorizerAttribute Build(AttributeData att)
{
var data = new FromCustomAuthorizerAttribute();
foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.Name) && pair.Value.Value is string value)
{
data.Name = value;
}
}

return data;
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,157 @@
validationErrors.Add($"Value {__request__.Body} at 'body' failed to satisfy constraint: {e.Message}");
}

<#
}
}
else if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromCustomAuthorizerAttribute))
{
var fromAuthorizerAttribute = parameter.Attributes?.FirstOrDefault(att => att.Type.FullName == TypeFullNames.FromCustomAuthorizerAttribute) as AttributeModel<Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute>;

// Use parameter name as key, if Name has not specified explicitly in the attribute definition.
var authKey = fromAuthorizerAttribute?.Data?.Name ?? parameter.Name;
// REST API and HTTP API v1 both use APIGatewayProxyRequest where RequestContext.Authorizer is a dictionary.
// Only HTTP API v2 uses APIGatewayHttpApiV2ProxyRequest with RequestContext.Authorizer.Lambda as the dictionary.
if(restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1)
{
#>
var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>);
if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("<#= authKey #>") == false)
{
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
{
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
{"x-amzn-ErrorType", "AccessDeniedException"}
},
StatusCode = 401
};
<#
if(_model.LambdaMethod.ReturnsIHttpResults)
{
#>
var __unauthorizedStream__ = new System.IO.MemoryStream();
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
__unauthorizedStream__.Position = 0;
return __unauthorizedStream__;
<#
}
else
{
#>
return __unauthorized__;
<#
}
#>
}

try
{
var __authValue_<#= parameter.Name #>__ = __request__.RequestContext.Authorizer["<#= authKey #>"];
<#= parameter.Name #> = (<#= parameter.Type.FullName #>)Convert.ChangeType(__authValue_<#= parameter.Name #>__?.ToString(), typeof(<#= parameter.Type.FullNameWithoutAnnotations #>));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why We Need .ToString() for Custom Authorizer Values

The Data Flow

  1. Authorizer Lambda returns context: When your custom authorizer runs, it returns a context dictionary:

    Context = new Dictionary<string, object>
    {
        { "userId", "12345" },
        { "permissions", "admin" }
    }
  2. API Gateway passes this to the protected Lambda in the request payload as JSON:

    {
      "requestContext": {
        "authorizer": {
          "userId": "12345",
          "permissions": "admin"
        }
      }
    }
  3. Lambda deserializes the request using the configured serializer (System.Text.Json or Newtonsoft.Json).

The Problem: Dictionary<string, object> Deserialization

The Authorizer property is typed as Dictionary<string, object>:

public class APIGatewayCustomAuthorizerContext : Dictionary<string, object>

When the serializer encounters a JSON value like "12345" and needs to deserialize it into object, it doesn't know what concrete type to use. So:

  • System.Text.Json wraps it in a JsonElement struct
  • Newtonsoft.Json wraps it in a JToken (like JValue)

so rather than having statements like


if (__authValue__ is System.Text.Json.JsonElement jsonElement)
{
    userId = Convert.ChangeType(jsonElement.ToString(), typeof(string));
}

i just call toString which both of these serializers have.

not sure if there is a better way

Copy link
Contributor

Choose a reason for hiding this comment

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

@normj not sure if you have any better ideas here of it ToString is good enough

}
catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException)
{
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
{
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
{"x-amzn-ErrorType", "AccessDeniedException"}
},
StatusCode = 401
};
<#
if(_model.LambdaMethod.ReturnsIHttpResults)
{
#>
var __unauthorizedStream__ = new System.IO.MemoryStream();
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
__unauthorizedStream__.Position = 0;
return __unauthorizedStream__;
<#
}
else
{
#>
return __unauthorized__;
<#
}
#>
}

<#
}
else
{
#>
var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>);
if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("<#= authKey #>") == false)
{
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
{
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
{"x-amzn-ErrorType", "AccessDeniedException"}
},
StatusCode = 401
};
<#
if(_model.LambdaMethod.ReturnsIHttpResults)
{
#>
var __unauthorizedStream__ = new System.IO.MemoryStream();
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
__unauthorizedStream__.Position = 0;
return __unauthorizedStream__;
<#
}
else
{
#>
return __unauthorized__;
<#
}
#>
}

try
{
var __authValue_<#= parameter.Name #>__ = __request__.RequestContext.Authorizer.Lambda["<#= authKey #>"];
<#= parameter.Name #> = (<#= parameter.Type.FullName #>)Convert.ChangeType(__authValue_<#= parameter.Name #>__?.ToString(), typeof(<#= parameter.Type.FullNameWithoutAnnotations #>));
}
catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException)
{
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
{
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
{"x-amzn-ErrorType", "AccessDeniedException"}
},
StatusCode = 401
};
<#
if(_model.LambdaMethod.ReturnsIHttpResults)
{
#>
var __unauthorizedStream__ = new System.IO.MemoryStream();
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
__unauthorizedStream__.Position = 0;
return __unauthorizedStream__;
<#
}
else
{
#>
return __unauthorized__;
<#
}
#>
}

<#
}
}
Expand Down Expand Up @@ -315,4 +466,4 @@

<#
}
#>
#>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static class TypeFullNames
public const string FromHeaderAttribute = "Amazon.Lambda.Annotations.APIGateway.FromHeaderAttribute";
public const string FromBodyAttribute = "Amazon.Lambda.Annotations.APIGateway.FromBodyAttribute";
public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute";
public const string FromCustomAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute";

public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent";
public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace Amazon.Lambda.Annotations.APIGateway
{
/// <summary>
/// Maps this parameter to a custom authorizer item
/// </summary>
/// <remarks>
/// Will try to get the specified key from Custom Authorizer values
/// </remarks>
[AttributeUsage(AttributeTargets.Parameter)]
public class FromCustomAuthorizerAttribute : Attribute, INamedAttribute
{
/// <summary>
/// Key of the value
/// </summary>
public string Name { get; set; }
}
}
18 changes: 17 additions & 1 deletion Libraries/src/Amazon.Lambda.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,22 @@ parameter to the `LambdaFunction` must be the event object and the event source
* Map method parameter to HTTP request body. If parameter is a complex type then request body will be assumed to be JSON and deserialized into the type.
* FromServices
* Map method parameter to registered service in IServiceProvider
* FromCustomAuthorizer
* Map method parameter to a custom authorizer context value. Use the `Name` property to specify the key in the authorizer context if it differs from the parameter name. Returns HTTP 401 Unauthorized if the key is not found or type conversion fails.

Example using `FromCustomAuthorizer` to access values set by a custom Lambda authorizer:
```csharp
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/api/protected")]
public async Task ProtectedEndpoint(
[FromCustomAuthorizer(Name = "userId")] string userId,
[FromCustomAuthorizer(Name = "tenantId")] int tenantId,
ILambdaContext context)
{
context.Logger.LogLine($"User {userId} from tenant {tenantId}");
// userId and tenantId are automatically extracted from the custom authorizer context
}
```

### Customizing responses for API Gateway Lambda functions

Expand Down Expand Up @@ -940,4 +956,4 @@ The content type is determined using the following rules.

## Project References

If API Gateway event attributes, such as `RestAPI` or `HttpAPI`, are being used then a package reference to `Amazon.Lambda.APIGatewayEvents` must be added to the project, otherwise the project will not compile. We do not include it by default in order to keep the `Amazon.Lambda.Annotations` library lightweight.
If API Gateway event attributes, such as `RestAPI` or `HttpAPI`, are being used then a package reference to `Amazon.Lambda.APIGatewayEvents` must be added to the project, otherwise the project will not compile. We do not include it by default in order to keep the `Amazon.Lambda.Annotations` library lightweight.
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
</ItemGroup>

<ItemGroup>
<None Remove="Snapshots\ServerlessTemplates\authorizerHttpApi.template" />
<None Remove="Snapshots\ServerlessTemplates\authorizerHttpApiV1.template" />
<None Remove="Snapshots\ServerlessTemplates\authorizerRest.template" />
<None Remove="Snapshots\ServerlessTemplates\customizeResponse.template" />
<None Remove="Snapshots\ServerlessTemplates\dynamicexample.template" />
<None Remove="Snapshots\ServerlessTemplates\intrinsicexample.template" />
Expand Down
Loading