From 0a9d27a4d1fcf5b168d3adf8aabb16dde415ab9b Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 8 Oct 2026 13:37:06 -0700 Subject: [PATCH 1/2] Fail closed in generated authorizers when parameter conversion fails Generated API Gateway authorizer handlers previously logged a conversion failure for [FromHeader], [FromQuery] and [FromRoute] parameters and then invoked the authorizer method with the parameter set to default(T). They now return a deny response matching the authorizer's return type without invoking the authorizer method. --- .../dd8d57a8-9a89-4cb1-bf10-f9699e781e7d.json | 11 + .../Templates/AuthorizerSetupParameters.cs | 147 ++++--- .../Templates/AuthorizerSetupParameters.tt | 25 ++ .../AuthorizerSetupParametersCode.cs | 101 +++++ .../TypeFullNames.cs | 2 + .../AuthorizerBindingFailureTests.cs | 404 ++++++++++++++++++ ...tion_SimpleHttpApiAuthorize_Generated.g.cs | 12 + ...tion_SimpleRestApiAuthorize_Generated.g.cs | 12 + ...ple_SimpleHttpApiAuthorizer_Generated.g.cs | 12 + ...ple_SimpleRestApiAuthorizer_Generated.g.cs | 12 + 10 files changed, 682 insertions(+), 56 deletions(-) create mode 100644 .autover/changes/dd8d57a8-9a89-4cb1-bf10-f9699e781e7d.json create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AuthorizerBindingFailureTests.cs diff --git a/.autover/changes/dd8d57a8-9a89-4cb1-bf10-f9699e781e7d.json b/.autover/changes/dd8d57a8-9a89-4cb1-bf10-f9699e781e7d.json new file mode 100644 index 000000000..5ffc83288 --- /dev/null +++ b/.autover/changes/dd8d57a8-9a89-4cb1-bf10-f9699e781e7d.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Annotations", + "Type": "Patch", + "ChangelogMessages": [ + "Generated API Gateway authorizer handlers now deny the request without invoking the authorizer method when a [FromHeader], [FromQuery] or [FromRoute] value cannot be converted to the parameter type, instead of invoking the method with the parameter set to its default value." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs index d61abbaaf..3289328bf 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs @@ -59,6 +59,21 @@ public virtual string TransformText() // Determine if the authorizer request type is V2 (has Headers as IDictionary) var isV2Request = httpApiAuthorizerAttribute != null && httpApiAuthorizerAttribute.Data.AuthorizerPayloadFormatVersion == Amazon.Lambda.Annotations.APIGateway.AuthorizerPayloadFormatVersion.V2; + // Track client supplied values that fail to convert so the authorizer fails closed instead of + // invoking the user's method with default values. + var hasBoundParameters = HasBoundParameters(); + if (hasBoundParameters) + { + + + #line default + #line hidden + this.Write(" var __bindingFailed__ = false;\r\n"); + + #line 42 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + + } + foreach (var parameter in _model.LambdaMethod.Parameters) { if (parameter.Type.FullName == TypeFullNames.ILambdaContext || TypeFullNames.AuthorizerRequests.Contains(parameter.Type.FullName)) @@ -73,21 +88,21 @@ public virtual string TransformText() #line hidden this.Write(" var "); - #line 44 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 54 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = scope.ServiceProvider.GetRequiredService<"); - #line 44 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 54 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(">();\r\n"); - #line 45 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 55 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } else if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromHeaderAttribute)) @@ -118,14 +133,14 @@ public virtual string TransformText() #line hidden this.Write(" var "); - #line 70 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 80 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = default("); - #line 70 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 80 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default @@ -133,21 +148,21 @@ public virtual string TransformText() this.Write(");\r\n if (!string.IsNullOrEmpty(__request__.AuthorizationToken))\r\n " + " {\r\n try\r\n {\r\n "); - #line 75 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 85 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = ("); - #line 75 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 85 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(")Convert.ChangeType(__request__.AuthorizationToken, typeof("); - #line 75 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 85 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullNameWithoutAnnotations)); #line default @@ -156,7 +171,7 @@ public virtual string TransformText() "tException || e is FormatException || e is OverflowException || e is ArgumentExc" + "eption)\r\n {\r\n"); - #line 79 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 89 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default @@ -164,7 +179,7 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.LogError(e, \"Failed to extract authoriza" + "tion token.\");\r\n"); - #line 81 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 91 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#else")); #line default @@ -172,14 +187,14 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.Log(\"Failed to extract authorization tok" + "en. Exception: \" + e.ToString());\r\n"); - #line 83 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 93 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#endif")); #line default #line hidden - this.Write("\r\n }\r\n }\r\n"); + this.Write("\r\n __bindingFailed__ = true;\r\n }\r\n }\r\n"); - #line 86 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 97 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } else @@ -191,21 +206,21 @@ public virtual string TransformText() #line hidden this.Write(" var "); - #line 92 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 103 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = default("); - #line 92 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 103 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(");\r\n if (__request__.Headers?.Any(x => string.Equals(x.Key, \""); - #line 93 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 104 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(headerKey)); #line default @@ -213,28 +228,28 @@ public virtual string TransformText() this.Write("\", StringComparison.OrdinalIgnoreCase)) == true)\r\n {\r\n " + "try\r\n {\r\n "); - #line 97 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 108 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = ("); - #line 97 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 108 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(")Convert.ChangeType(__request__.Headers.First(x => string.Equals(x.Key, \""); - #line 97 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 108 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(headerKey)); #line default #line hidden this.Write("\", StringComparison.OrdinalIgnoreCase)).Value, typeof("); - #line 97 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 108 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullNameWithoutAnnotations)); #line default @@ -243,42 +258,42 @@ public virtual string TransformText() "tException || e is FormatException || e is OverflowException || e is ArgumentExc" + "eption)\r\n {\r\n"); - #line 101 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 112 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden this.Write("\r\n __context__.Logger.LogError(e, \"Failed to extract header \'"); - #line 102 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 113 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(headerKey)); #line default #line hidden this.Write("\'.\");\r\n"); - #line 103 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 114 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#else")); #line default #line hidden this.Write("\r\n __context__.Logger.Log(\"Failed to extract header \'"); - #line 104 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 115 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(headerKey)); #line default #line hidden this.Write("\'. Exception: \" + e.ToString());\r\n"); - #line 105 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 116 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#endif")); #line default #line hidden - this.Write("\r\n }\r\n }\r\n"); + this.Write("\r\n __bindingFailed__ = true;\r\n }\r\n }\r\n"); - #line 108 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 120 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } @@ -287,7 +302,7 @@ public virtual string TransformText() #line hidden this.Write("\r\n"); - #line 112 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 124 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } else if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute)) @@ -300,21 +315,21 @@ public virtual string TransformText() #line hidden this.Write(" var "); - #line 119 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 131 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = default("); - #line 119 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 131 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(");\r\n if (__request__.QueryStringParameters?.ContainsKey(\""); - #line 120 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 132 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameterKey)); #line default @@ -322,28 +337,28 @@ public virtual string TransformText() this.Write("\") == true)\r\n {\r\n try\r\n {\r\n " + " "); - #line 124 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 136 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = ("); - #line 124 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 136 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(")Convert.ChangeType(__request__.QueryStringParameters[\""); - #line 124 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 136 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameterKey)); #line default #line hidden this.Write("\"], typeof("); - #line 124 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 136 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullNameWithoutAnnotations)); #line default @@ -352,7 +367,7 @@ public virtual string TransformText() "tException || e is FormatException || e is OverflowException || e is ArgumentExc" + "eption)\r\n {\r\n"); - #line 128 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 140 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default @@ -360,14 +375,14 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.LogError(e, \"Failed to extract query par" + "ameter \'"); - #line 129 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 141 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameterKey)); #line default #line hidden this.Write("\'.\");\r\n"); - #line 130 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 142 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#else")); #line default @@ -375,21 +390,21 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.Log(\"Failed to extract query parameter \'" + ""); - #line 131 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 143 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameterKey)); #line default #line hidden this.Write("\'. Exception: \" + e.ToString());\r\n"); - #line 132 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 144 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#endif")); #line default #line hidden - this.Write("\r\n }\r\n }\r\n\r\n"); + this.Write("\r\n __bindingFailed__ = true;\r\n }\r\n }\r\n\r\n"); - #line 136 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 149 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } else if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromRouteAttribute)) @@ -402,21 +417,21 @@ public virtual string TransformText() #line hidden this.Write(" var "); - #line 143 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 156 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = default("); - #line 143 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 156 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(");\r\n if (__request__.PathParameters?.ContainsKey(\""); - #line 144 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 157 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(routeKey)); #line default @@ -424,28 +439,28 @@ public virtual string TransformText() this.Write("\") == true)\r\n {\r\n try\r\n {\r\n " + " "); - #line 148 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 161 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Name)); #line default #line hidden this.Write(" = ("); - #line 148 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 161 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullName)); #line default #line hidden this.Write(")Convert.ChangeType(__request__.PathParameters[\""); - #line 148 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 161 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(routeKey)); #line default #line hidden this.Write("\"], typeof("); - #line 148 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 161 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(parameter.Type.FullNameWithoutAnnotations)); #line default @@ -454,7 +469,7 @@ public virtual string TransformText() "tException || e is FormatException || e is OverflowException || e is ArgumentExc" + "eption)\r\n {\r\n"); - #line 152 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 165 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default @@ -462,14 +477,14 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.LogError(e, \"Failed to extract route par" + "ameter \'"); - #line 153 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 166 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(routeKey)); #line default #line hidden this.Write("\'.\");\r\n"); - #line 154 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 167 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#else")); #line default @@ -477,21 +492,21 @@ public virtual string TransformText() this.Write("\r\n __context__.Logger.Log(\"Failed to extract route parameter \'" + ""); - #line 155 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 168 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture(routeKey)); #line default #line hidden this.Write("\'. Exception: \" + e.ToString());\r\n"); - #line 156 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 169 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" this.Write(this.ToStringHelper.ToStringWithCulture("#endif")); #line default #line hidden - this.Write("\r\n }\r\n }\r\n\r\n"); + this.Write("\r\n __bindingFailed__ = true;\r\n }\r\n }\r\n\r\n"); - #line 160 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + #line 174 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" } else @@ -500,6 +515,26 @@ public virtual string TransformText() } } + if (hasBoundParameters) + { + + + #line default + #line hidden + this.Write(" // Deny the request if any client supplied value failed to convert to" + + " its parameter type.\r\n if (__bindingFailed__)\r\n {\r\n"); + + #line 188 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + this.Write(this.ToStringHelper.ToStringWithCulture(GenerateBindingFailureResponse())); + + #line default + #line hidden + this.Write(" }\r\n\r\n"); + + #line 190 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" + + } + #line default #line hidden diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt index 9745212f9..6fb9b9283 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt @@ -32,6 +32,16 @@ // Determine if the authorizer request type is V2 (has Headers as IDictionary) var isV2Request = httpApiAuthorizerAttribute != null && httpApiAuthorizerAttribute.Data.AuthorizerPayloadFormatVersion == Amazon.Lambda.Annotations.APIGateway.AuthorizerPayloadFormatVersion.V2; + // Track client supplied values that fail to convert so the authorizer fails closed instead of + // invoking the user's method with default values. + var hasBoundParameters = HasBoundParameters(); + if (hasBoundParameters) + { +#> + var __bindingFailed__ = false; +<# + } + foreach (var parameter in _model.LambdaMethod.Parameters) { if (parameter.Type.FullName == TypeFullNames.ILambdaContext || TypeFullNames.AuthorizerRequests.Contains(parameter.Type.FullName)) @@ -81,6 +91,7 @@ <#= "#else" #> __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); <#= "#endif" #> + __bindingFailed__ = true; } } <# @@ -103,6 +114,7 @@ <#= "#else" #> __context__.Logger.Log("Failed to extract header '<#= headerKey #>'. Exception: " + e.ToString()); <#= "#endif" #> + __bindingFailed__ = true; } } <# @@ -130,6 +142,7 @@ <#= "#else" #> __context__.Logger.Log("Failed to extract query parameter '<#= parameterKey #>'. Exception: " + e.ToString()); <#= "#endif" #> + __bindingFailed__ = true; } } @@ -154,6 +167,7 @@ <#= "#else" #> __context__.Logger.Log("Failed to extract route parameter '<#= routeKey #>'. Exception: " + e.ToString()); <#= "#endif" #> + __bindingFailed__ = true; } } @@ -164,4 +178,15 @@ throw new NotSupportedException($"{parameter.Name} parameter of type {parameter.Type.FullName} passing is not supported for authorizer functions."); } } + + if (hasBoundParameters) + { +#> + // Deny the request if any client supplied value failed to convert to its parameter type. + if (__bindingFailed__) + { +<#= GenerateBindingFailureResponse() #> } + +<# + } #> diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParametersCode.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParametersCode.cs index 412d65b9c..6da2b1a92 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParametersCode.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParametersCode.cs @@ -1,4 +1,7 @@ +using System.Linq; +using System.Text; using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; namespace Amazon.Lambda.Annotations.SourceGenerator.Templates { @@ -12,5 +15,103 @@ public AuthorizerSetupParameters(LambdaFunctionModel model) { _model = model; } + + /// + /// Returns true if any parameter is bound from a client supplied value (header, query string or route) + /// and therefore requires conversion that may fail. + /// + private bool HasBoundParameters() + { + return _model.LambdaMethod.Parameters.Any(p => + p.Type.FullName != TypeFullNames.ILambdaContext && + !TypeFullNames.AuthorizerRequests.Contains(p.Type.FullName) && + !p.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromServiceAttribute) && + p.Attributes.Any(att => + att.Type.FullName == TypeFullNames.FromHeaderAttribute || + att.Type.FullName == TypeFullNames.FromQueryAttribute || + att.Type.FullName == TypeFullNames.FromRouteAttribute)); + } + + /// + /// Generates the statements that deny the request when a client supplied value fails to convert to the + /// parameter type. The user's authorizer method is not invoked so it never evaluates default values + /// in place of the client supplied value. + /// + private string GenerateBindingFailureResponse() + { + const string indent = " "; + + var httpApiAuthorizerAttribute = _model.LambdaMethod.Attributes.FirstOrDefault(att => att.Type.FullName == TypeFullNames.HttpApiAuthorizerAttribute) as AttributeModel; + var isV2Request = httpApiAuthorizerAttribute != null && httpApiAuthorizerAttribute.Data.AuthorizerPayloadFormatVersion == Amazon.Lambda.Annotations.APIGateway.AuthorizerPayloadFormatVersion.V2; + var methodArnExpression = isV2Request ? "__request__.RouteArn" : "__request__.MethodArn"; + + var sb = new StringBuilder(); + if (_model.LambdaMethod.ReturnsIAuthorizerResult) + { + string format; + if (httpApiAuthorizerAttribute != null && httpApiAuthorizerAttribute.Data.EnableSimpleResponses) + { + format = "AuthorizerResultSerializationOptions.AuthorizerFormat.HttpApiSimple"; + } + else if (httpApiAuthorizerAttribute != null) + { + format = "AuthorizerResultSerializationOptions.AuthorizerFormat.HttpApiIamPolicy"; + } + else + { + format = "AuthorizerResultSerializationOptions.AuthorizerFormat.RestApi"; + } + + AppendLine(sb, $"{indent}return AuthorizerResults.Deny().Serialize(new AuthorizerResultSerializationOptions"); + AppendLine(sb, $"{indent}{{"); + AppendLine(sb, $"{indent} Format = {format},"); + AppendLine(sb, $"{indent} MethodArn = {methodArnExpression}"); + AppendLine(sb, $"{indent}}});"); + return sb.ToString(); + } + + var returnType = _model.LambdaMethod.ReturnsGenericTask && _model.LambdaMethod.ReturnType.TypeArguments.Count == 1 + ? _model.LambdaMethod.ReturnType.TypeArguments[0].FullName + : _model.LambdaMethod.ReturnType.FullName; + + if (returnType == TypeFullNames.APIGatewayCustomAuthorizerV2SimpleResponse) + { + AppendLine(sb, $"{indent}return new {TypeFullNames.APIGatewayCustomAuthorizerV2SimpleResponse} {{ IsAuthorized = false }};"); + } + else if (returnType == TypeFullNames.APIGatewayCustomAuthorizerResponse || returnType == TypeFullNames.APIGatewayCustomAuthorizerV2IamResponse) + { + AppendLine(sb, $"{indent}return new {returnType}"); + AppendLine(sb, $"{indent}{{"); + AppendLine(sb, $"{indent} PrincipalID = \"user\","); + AppendLine(sb, $"{indent} PolicyDocument = new {TypeFullNames.APIGatewayCustomAuthorizerPolicy}"); + AppendLine(sb, $"{indent} {{"); + AppendLine(sb, $"{indent} Statement = new List<{TypeFullNames.APIGatewayCustomAuthorizerPolicy}.IAMPolicyStatement>"); + AppendLine(sb, $"{indent} {{"); + AppendLine(sb, $"{indent} new {TypeFullNames.APIGatewayCustomAuthorizerPolicy}.IAMPolicyStatement"); + AppendLine(sb, $"{indent} {{"); + AppendLine(sb, $"{indent} Effect = \"Deny\","); + AppendLine(sb, $"{indent} Action = new HashSet {{ \"execute-api:Invoke\" }},"); + AppendLine(sb, $"{indent} Resource = new HashSet {{ {methodArnExpression} ?? \"*\" }}"); + AppendLine(sb, $"{indent} }}"); + AppendLine(sb, $"{indent} }}"); + AppendLine(sb, $"{indent} }}"); + AppendLine(sb, $"{indent}}};"); + } + else + { + // Unknown response shape. API Gateway maps an "Unauthorized" error from the authorizer to a 401 response. + AppendLine(sb, $"{indent}throw new Exception(\"Unauthorized\");"); + } + + return sb.ToString(); + } + + /// + /// Appends a line using the same line ending the T4 templates emit so the generated code is consistent. + /// + private static void AppendLine(StringBuilder sb, string line) + { + sb.Append(line).Append("\r\n"); + } } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index 6662f0c94..ceebf32ce 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -47,6 +47,8 @@ public static class TypeFullNames public const string APIGatewayCustomAuthorizerRequest = "Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest"; public const string APIGatewayCustomAuthorizerV2SimpleResponse = "Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2SimpleResponse"; public const string APIGatewayCustomAuthorizerResponse = "Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerResponse"; + public const string APIGatewayCustomAuthorizerV2IamResponse = "Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2IamResponse"; + public const string APIGatewayCustomAuthorizerPolicy = "Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerPolicy"; public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent"; public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse"; diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AuthorizerBindingFailureTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AuthorizerBindingFailureTests.cs new file mode 100644 index 000000000..5fd630d45 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AuthorizerBindingFailureTests.cs @@ -0,0 +1,404 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + /// + /// Runs the source generator over authorizers with value typed parameters, compiles the generated handlers and + /// invokes them. A client supplied value that fails to convert must deny the request without invoking the user's + /// authorizer method, otherwise the user's method would evaluate default(T) in place of the client supplied value. + /// + public class AuthorizerBindingFailureTests + { + private const string UserSource = @" +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.APIGatewayEvents; + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace TestApp +{ + // Every authorizer allows the request so a fail open binding failure is observable as an allow. + public class Authorizers + { + public static List Invocations = new List(); + + [LambdaFunction] + [HttpApiAuthorizer(EnableSimpleResponses = true)] + public IAuthorizerResult HttpApiSimple([FromHeader(Name = ""X-Org-Id"")] long orgId, ILambdaContext context) + { + Invocations.Add(nameof(HttpApiSimple) + "":"" + orgId); + return AuthorizerResults.Allow(); + } + + [LambdaFunction] + [HttpApiAuthorizer(EnableSimpleResponses = false)] + public async Task HttpApiIamPolicy([FromQuery] bool admin, ILambdaContext context) + { + await Task.Yield(); + Invocations.Add(nameof(HttpApiIamPolicy) + "":"" + admin); + return AuthorizerResults.Allow(); + } + + [LambdaFunction] + [RestApiAuthorizer(Type = RestApiAuthorizerType.Token)] + public IAuthorizerResult RestApiToken([FromHeader(Name = ""Authorization"")] int token, ILambdaContext context) + { + Invocations.Add(nameof(RestApiToken) + "":"" + token); + return AuthorizerResults.Allow(); + } + + [LambdaFunction] + [RestApiAuthorizer(Type = RestApiAuthorizerType.Request)] + public APIGatewayCustomAuthorizerResponse RestApiRequestRaw([FromRoute] int tenantId, APIGatewayCustomAuthorizerRequest request, ILambdaContext context) + { + Invocations.Add(nameof(RestApiRequestRaw) + "":"" + tenantId); + return Allow(request.MethodArn); + } + + [LambdaFunction] + [HttpApiAuthorizer(EnableSimpleResponses = true)] + public async Task HttpApiSimpleRaw([FromHeader(Name = ""X-Org-Id"")] int orgId, ILambdaContext context) + { + await Task.Yield(); + Invocations.Add(nameof(HttpApiSimpleRaw) + "":"" + orgId); + return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = true }; + } + + [LambdaFunction] + [HttpApiAuthorizer(EnableSimpleResponses = false)] + public APIGatewayCustomAuthorizerV2IamResponse HttpApiIamRaw([FromHeader(Name = ""X-Org-Id"")] long orgId, [FromQuery] string name, APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) + { + Invocations.Add(nameof(HttpApiIamRaw) + "":"" + orgId); + return new APIGatewayCustomAuthorizerV2IamResponse { PolicyDocument = Allow(request.RouteArn).PolicyDocument }; + } + + [LambdaFunction] + [HttpApiAuthorizer(EnableSimpleResponses = true)] + public Dictionary HttpApiCustomRaw([FromHeader(Name = ""X-Org-Id"")] long orgId, ILambdaContext context) + { + Invocations.Add(nameof(HttpApiCustomRaw) + "":"" + orgId); + return new Dictionary { { ""isAuthorized"", true } }; + } + + private static APIGatewayCustomAuthorizerResponse Allow(string arn) + { + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = ""user"", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Effect = ""Allow"", + Action = new HashSet { ""execute-api:Invoke"" }, + Resource = new HashSet { arn } + } + } + } + }; + } + } +} +"; + + private const string MethodArn = "arn:aws:execute-api:us-west-2:123456789012:abcdef/prod/GET/data"; + + private static readonly Lazy GeneratedAssembly = new Lazy(CompileWithGenerator); + + public AuthorizerBindingFailureTests() + { + // Tests within a class run sequentially so the static invocation record can be reset per test. + Invocations().Clear(); + } + + public static IEnumerable MalformedValues => new[] + { + new object[] { "abc" }, + new object[] { "99999999999999999999" }, + new object[] { "" }, + }; + + [Theory] + [MemberData(nameof(MalformedValues))] + public async Task HttpApiSimple_IAuthorizerResult_DeniesOnMalformedHeader(string value) + { + var request = new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "X-Org-Id", value } } + }; + + var response = await InvokeAsync("HttpApiSimple", request); + + Assert.False(ReadJson((Stream)response).RootElement.GetProperty("isAuthorized").GetBoolean()); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("HttpApiSimple:")); + } + + [Fact] + public async Task HttpApiIamPolicy_IAuthorizerResult_DeniesOnMalformedQuery() + { + var request = new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + QueryStringParameters = new Dictionary { { "admin", "yes" } } + }; + + var response = await InvokeAsync("HttpApiIamPolicy", request); + + AssertDenyPolicyJson((Stream)response); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("HttpApiIamPolicy:")); + } + + [Fact] + public async Task RestApiToken_IAuthorizerResult_DeniesOnMalformedToken() + { + var request = new APIGatewayCustomAuthorizerRequest + { + Type = "TOKEN", + MethodArn = MethodArn, + AuthorizationToken = "Bearer abc" + }; + + var response = await InvokeAsync("RestApiToken", request); + + AssertDenyPolicyJson((Stream)response); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("RestApiToken:")); + } + + [Fact] + public async Task RestApiRequest_RawResponse_DeniesOnMalformedRoute() + { + var request = new APIGatewayCustomAuthorizerRequest + { + Type = "REQUEST", + MethodArn = MethodArn, + PathParameters = new Dictionary { { "tenantId", "1.5" } } + }; + + var response = (APIGatewayCustomAuthorizerResponse)await InvokeAsync("RestApiRequestRaw", request); + + AssertDenyPolicy(response.PolicyDocument); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("RestApiRequestRaw:")); + } + + [Fact] + public async Task HttpApiSimple_RawResponse_DeniesOnMalformedHeader() + { + var request = new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "x-org-id", "abc" } } + }; + + var response = (APIGatewayCustomAuthorizerV2SimpleResponse)await InvokeAsync("HttpApiSimpleRaw", request); + + Assert.False(response.IsAuthorized); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("HttpApiSimpleRaw:")); + } + + [Fact] + public async Task HttpApiIam_RawResponse_DeniesOnMalformedHeader() + { + var request = new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "X-Org-Id", "abc" } } + }; + + var response = (APIGatewayCustomAuthorizerV2IamResponse)await InvokeAsync("HttpApiIamRaw", request); + + AssertDenyPolicy(response.PolicyDocument); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("HttpApiIamRaw:")); + } + + [Fact] + public async Task CustomRawResponse_ThrowsUnauthorizedOnMalformedHeader() + { + var request = new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "X-Org-Id", "abc" } } + }; + + var exception = await Assert.ThrowsAsync(() => InvokeAsync("HttpApiCustomRaw", request)); + + Assert.Equal("Unauthorized", exception.Message); + Assert.DoesNotContain(Invocations(), i => i.StartsWith("HttpApiCustomRaw:")); + } + + [Fact] + public async Task WellFormedValues_InvokeUserMethod() + { + var httpSimple = await InvokeAsync("HttpApiSimple", new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "X-Org-Id", "42" } } + }); + Assert.True(ReadJson((Stream)httpSimple).RootElement.GetProperty("isAuthorized").GetBoolean()); + Assert.Contains("HttpApiSimple:42", Invocations()); + + var restRaw = (APIGatewayCustomAuthorizerResponse)await InvokeAsync("RestApiRequestRaw", new APIGatewayCustomAuthorizerRequest + { + Type = "REQUEST", + MethodArn = MethodArn, + PathParameters = new Dictionary { { "tenantId", "7" } } + }); + Assert.Equal("Allow", restRaw.PolicyDocument.Statement.Single().Effect); + Assert.Contains("RestApiRequestRaw:7", Invocations()); + + // A string parameter cannot fail conversion so it never triggers the deny path. + var httpIamRaw = (APIGatewayCustomAuthorizerV2IamResponse)await InvokeAsync("HttpApiIamRaw", new APIGatewayCustomAuthorizerV2Request + { + RouteArn = MethodArn, + Headers = new Dictionary { { "X-Org-Id", "5" } }, + QueryStringParameters = new Dictionary { { "name", "abc" } } + }); + Assert.Equal("Allow", httpIamRaw.PolicyDocument.Statement.Single().Effect); + Assert.Contains("HttpApiIamRaw:5", Invocations()); + } + + private static void AssertDenyPolicy(APIGatewayCustomAuthorizerPolicy policy) + { + var statement = Assert.Single(policy.Statement); + Assert.Equal("Deny", statement.Effect); + Assert.Equal(new[] { "execute-api:Invoke" }, statement.Action); + Assert.Equal(new[] { MethodArn }, statement.Resource); + } + + private static void AssertDenyPolicyJson(Stream response) + { + var statement = ReadJson(response).RootElement.GetProperty("policyDocument").GetProperty("Statement").EnumerateArray().Single(); + Assert.Equal("Deny", statement.GetProperty("Effect").GetString()); + Assert.Equal(MethodArn, statement.GetProperty("Resource").GetString()); + } + + private static JsonDocument ReadJson(Stream stream) + { + return JsonDocument.Parse(stream); + } + + private static List Invocations() + { + var type = GeneratedAssembly.Value.GetType("TestApp.Authorizers", throwOnError: true); + return (List)type.GetField("Invocations").GetValue(null); + } + + private static async Task InvokeAsync(string methodName, object request) + { + var type = GeneratedAssembly.Value.GetType($"TestApp.Authorizers_{methodName}_Generated", throwOnError: true); + var instance = Activator.CreateInstance(type); + var method = type.GetMethod(methodName); + + object result; + try + { + result = method.Invoke(instance, new[] { request, new TestLambdaContext() }); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + + if (result is Task task) + { + await task; + return task.GetType().GetProperty("Result").GetValue(task); + } + + return result; + } + + private static Assembly CompileWithGenerator() + { + // The generator only runs when the source file is inside a project directory and writes the + // serverless.template there, so use an isolated temporary project directory. + var projectDirectory = Path.Combine(Path.GetTempPath(), "AuthorizerBindingFailureTests-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDirectory); + File.WriteAllText(Path.Combine(projectDirectory, "TestApp.csproj"), ""); + var sourcePath = Path.Combine(projectDirectory, "Authorizers.cs"); + File.WriteAllText(sourcePath, UserSource); + + var parseOptions = new CSharpParseOptions(LanguageVersion.Latest, preprocessorSymbols: new[] { "NET8_0_OR_GREATER" }); + var compilation = CSharpCompilation.Create( + "AuthorizerBindingFailureTests", + new[] { CSharpSyntaxTree.ParseText(UserSource, parseOptions, sourcePath) }, + BuildReferences(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + CSharpGeneratorDriver + .Create(new[] { new SourceGenerator.Generator() }, parseOptions: parseOptions) + .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); + + Directory.Delete(projectDirectory, recursive: true); + + using var stream = new MemoryStream(); + var result = outputCompilation.Emit(stream); + var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.True(outputCompilation.SyntaxTrees.Count() > 1, "Source generator did not produce any output:\n" + string.Join("\n", generatorDiagnostics)); + Assert.True(result.Success, "Generated code failed to compile:\n" + string.Join("\n", errors)); + + return Assembly.Load(stream.ToArray()); + } + + private static IReadOnlyList BuildReferences() + { + var references = new List(); + var trusted = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? string.Empty) + .Split(Path.PathSeparator); + foreach (var path in trusted) + { + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + references.Add(MetadataReference.CreateFromFile(path)); + } + + references.Add(MetadataReference.CreateFromFile(typeof(ILambdaContext).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(APIGatewayCustomAuthorizerRequest).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(APIGateway.AuthorizerResults).Assembly.Location)); + references.Add(MetadataReference.CreateFromFile(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer).Assembly.Location)); + return references.GroupBy(r => Path.GetFileName(r.Display)).Select(g => g.Last()).ToList(); + } + + private class TestLambdaContext : ILambdaContext + { + public string AwsRequestId => "request-id"; + public IClientContext ClientContext => null; + public string FunctionName => "authorizer"; + public string FunctionVersion => "$LATEST"; + public ICognitoIdentity Identity => null; + public string InvokedFunctionArn => null; + public ILambdaLogger Logger { get; } = new TestLambdaLogger(); + public string LogGroupName => null; + public string LogStreamName => null; + public int MemoryLimitInMB => 128; + public TimeSpan RemainingTime => TimeSpan.FromMinutes(1); + } + + private class TestLambdaLogger : ILambdaLogger + { + public void Log(string message) { } + public void LogLine(string message) { } + public void Log(string level, string message, params object[] args) { } + public void Log(string level, Exception exception, string message, params object[] args) { } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs index e16453b99..d5574ab2e 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs @@ -39,6 +39,7 @@ public AuthorizerFunction_SimpleHttpApiAuthorize_Generated() /// Result of the Lambda function execution public System.IO.Stream SimpleHttpApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2Request __request__, Amazon.Lambda.Core.ILambdaContext __context__) { + var __bindingFailed__ = false; var authorization = default(string); if (__request__.Headers?.Any(x => string.Equals(x.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) == true) { @@ -53,9 +54,20 @@ public System.IO.Stream SimpleHttpApiAuthorize(Amazon.Lambda.APIGatewayEvents.AP #else __context__.Logger.Log("Failed to extract header 'Authorization'. Exception: " + e.ToString()); #endif + __bindingFailed__ = true; } } + // Deny the request if any client supplied value failed to convert to its parameter type. + if (__bindingFailed__) + { + return AuthorizerResults.Deny().Serialize(new AuthorizerResultSerializationOptions + { + Format = AuthorizerResultSerializationOptions.AuthorizerFormat.HttpApiSimple, + MethodArn = __request__.RouteArn + }); + } + var authorizerResult = authorizerFunction.SimpleHttpApiAuthorize(authorization, __context__); var serializationOptions = new AuthorizerResultSerializationOptions { diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs index b78ba8321..c97abbf43 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs @@ -39,6 +39,7 @@ public AuthorizerFunction_SimpleRestApiAuthorize_Generated() /// Result of the Lambda function execution public System.IO.Stream SimpleRestApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) { + var __bindingFailed__ = false; var authorization = default(string); if (!string.IsNullOrEmpty(__request__.AuthorizationToken)) { @@ -53,9 +54,20 @@ public System.IO.Stream SimpleRestApiAuthorize(Amazon.Lambda.APIGatewayEvents.AP #else __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); #endif + __bindingFailed__ = true; } } + // Deny the request if any client supplied value failed to convert to its parameter type. + if (__bindingFailed__) + { + return AuthorizerResults.Deny().Serialize(new AuthorizerResultSerializationOptions + { + Format = AuthorizerResultSerializationOptions.AuthorizerFormat.RestApi, + MethodArn = __request__.MethodArn + }); + } + var authorizerResult = authorizerFunction.SimpleRestApiAuthorize(authorization, __context__); var serializationOptions = new AuthorizerResultSerializationOptions { diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs index d8c34394c..71ff3effa 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs @@ -39,6 +39,7 @@ public IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated() /// Result of the Lambda function execution public System.IO.Stream SimpleHttpApiAuthorizer(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2Request __request__, Amazon.Lambda.Core.ILambdaContext __context__) { + var __bindingFailed__ = false; var authorization = default(string); if (__request__.Headers?.Any(x => string.Equals(x.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) == true) { @@ -53,9 +54,20 @@ public System.IO.Stream SimpleHttpApiAuthorizer(Amazon.Lambda.APIGatewayEvents.A #else __context__.Logger.Log("Failed to extract header 'Authorization'. Exception: " + e.ToString()); #endif + __bindingFailed__ = true; } } + // Deny the request if any client supplied value failed to convert to its parameter type. + if (__bindingFailed__) + { + return AuthorizerResults.Deny().Serialize(new AuthorizerResultSerializationOptions + { + Format = AuthorizerResultSerializationOptions.AuthorizerFormat.HttpApiSimple, + MethodArn = __request__.RouteArn + }); + } + var authorizerResult = iAuthorizerResultExample.SimpleHttpApiAuthorizer(authorization, __context__); var serializationOptions = new AuthorizerResultSerializationOptions { diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs index e49da0a08..c514d6538 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs @@ -39,6 +39,7 @@ public IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated() /// Result of the Lambda function execution public System.IO.Stream SimpleRestApiAuthorizer(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) { + var __bindingFailed__ = false; var authorization = default(string); if (!string.IsNullOrEmpty(__request__.AuthorizationToken)) { @@ -53,9 +54,20 @@ public System.IO.Stream SimpleRestApiAuthorizer(Amazon.Lambda.APIGatewayEvents.A #else __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); #endif + __bindingFailed__ = true; } } + // Deny the request if any client supplied value failed to convert to its parameter type. + if (__bindingFailed__) + { + return AuthorizerResults.Deny().Serialize(new AuthorizerResultSerializationOptions + { + Format = AuthorizerResultSerializationOptions.AuthorizerFormat.RestApi, + MethodArn = __request__.MethodArn + }); + } + var authorizerResult = iAuthorizerResultExample.SimpleRestApiAuthorizer(authorization, __context__); var serializationOptions = new AuthorizerResultSerializationOptions { From a44fea4b3a22a04a4151ed72cca1c1110e03e260 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 8 Oct 2026 14:44:26 -0700 Subject: [PATCH 2/2] Add skill for Annotation --- .agents/skills/lambda-annotations/SKILL.md | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .agents/skills/lambda-annotations/SKILL.md diff --git a/.agents/skills/lambda-annotations/SKILL.md b/.agents/skills/lambda-annotations/SKILL.md new file mode 100644 index 000000000..349e38b31 --- /dev/null +++ b/.agents/skills/lambda-annotations/SKILL.md @@ -0,0 +1,81 @@ +--- +name: lambda-annotations +description: Conventions and safety rules for changing the Amazon.Lambda.Annotations library and its source generator, including the T4 templates that generate Lambda handler code. Use when modifying parameter binding, generated handler code, authorizer support, or the source generator templates. +--- + +# Working on Amazon.Lambda.Annotations + +This skill covers rules for changing the Amazon.Lambda.Annotations source generator. The code it generates is compiled into customer assemblies. A bug in a template becomes a bug in every customer function built with that version, and a fix only reaches customers when they rebuild and redeploy. + +## Key Locations + +- **Attributes and runtime types**: `Libraries/src/Amazon.Lambda.Annotations/` +- **Source generator**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/` +- **Templates**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/` + - `*.tt`: T4 templates (the source of truth) + - `*.cs` with the same name as a `.tt`: preprocessed output from `TextTemplatingFilePreprocessor`. Keep it in sync with the `.tt`. + - `*Code.cs`: hand-written partial classes for the templates. Put non-trivial generation logic here rather than in the `.tt` so the preprocessed `.cs` changes stay small. +- **Source generator tests**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/` + - `Snapshots/`: expected generated code. Update these when template output changes. + - `AuthorizerBindingFailureTests.cs`: runs the generator, compiles the output and invokes the generated handlers. Use it as the pattern for runtime behavior tests of generated code. + +## Template Editing + +- Edit the `.tt` and the matching preprocessed `.cs` together. Regenerating with Visual Studio is preferred. If you edit the `.cs` by hand, mirror the `.tt` exactly and keep the `this.Write(...)` strings using `\r\n` line endings like the rest of the file. +- Generated code that is emitted from `*Code.cs` helpers should also use `\r\n` line endings to match the T4 output. +- After changing templates, run the source generator tests on both target frameworks: + ``` + cd Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests + dotnet test + ``` +- Building the test projects rewrites the `serverless.template` files under `Libraries/test/*` with the current Annotations version. Revert those changes unless the template change is intentional. + +## Parameter Binding Must Never Fail Silently + +The templates convert client-supplied strings (headers, query string, route parameters, authorization tokens, authorizer context values) to the parameter type with `Convert.ChangeType`. **Every `Convert.ChangeType` failure must change the outcome of the request. Logging the failure and continuing is not acceptable.** + +If a conversion failure is only logged, the parameter keeps its `default(T)` value (`0`, `false`, `MinValue`, `null`) and the user's method runs with a value the client never sent. For any security decision, an attacker can then choose that value by sending something that doesn't parse. + +Required behavior by handler type: + +| Handler type | Template | On conversion failure | +|---|---|---| +| API Gateway (`[RestApi]`, `[HttpApi]`) `[FromHeader]`, `[FromQuery]`, `[FromRoute]`, `[FromBody]` | `APIGatewaySetupParameters.tt` | Add to `validationErrors` and return **400** without invoking the user's method | +| API Gateway `[FromCustomAuthorizer]` | `APIGatewaySetupParameters.tt` | Return **401** without invoking the user's method | +| ALB | `ALBSetupParameters.tt` | Add to `validationErrors` and return **400** without invoking the user's method | +| Authorizers (`[HttpApiAuthorizer]`, `[RestApiAuthorizer]`) | `AuthorizerSetupParameters.tt` | **Deny** without invoking the user's authorizer method | + +### Authorizers Must Fail Closed + +An authorizer that runs on a default value can grant access. When binding fails, authorizer templates must deny the request: + +- Each conversion `catch` block in `AuthorizerSetupParameters.tt` sets `__bindingFailed__ = true;` after logging. +- After all parameters are bound, the generated code checks `__bindingFailed__` and returns the deny response from `GenerateBindingFailureResponse()` in `AuthorizerSetupParametersCode.cs`. The deny response matches the authorizer's return type: + - `IAuthorizerResult`: `AuthorizerResults.Deny()` serialized with the same format and method/route ARN as the normal path + - `APIGatewayCustomAuthorizerV2SimpleResponse`: `IsAuthorized = false` + - `APIGatewayCustomAuthorizerResponse` / `APIGatewayCustomAuthorizerV2IamResponse`: an explicit `Deny` policy + - Any other type: `throw new Exception("Unauthorized")` +- The `__bindingFailed__` declaration and the deny check are both emitted only when `HasBoundParameters()` is true. If you add a new binding source, such as a new `[From*]` attribute or a new conversion branch, update `HasBoundParameters()`. If you forget, the generated code fails to compile because the flag is set but never declared. That's loud, but it fails in customer builds too. +- The dangerous mistake is a new conversion `catch` that doesn't set `__bindingFailed__`. That silently fails open and nothing catches it unless a test sends a malformed value. + +### Checklist for Binding Changes + +When adding or changing any code path that converts a client-supplied value: + +1. Confirm the `catch` block changes the outcome of the request (validation error, 401 or deny) and doesn't only log. +2. For authorizers, confirm the `catch` sets `__bindingFailed__ = true;` and that `HasBoundParameters()` covers the new parameter source. +3. Quick audit of the authorizer template. The three counts should match: + ``` + grep -c "Convert.ChangeType" Templates/AuthorizerSetupParameters.tt + grep -c "catch (Exception e)" Templates/AuthorizerSetupParameters.tt + grep -c "__bindingFailed__ = true;" Templates/AuthorizerSetupParameters.tt + ``` +4. Add a runtime test with a malformed value (for example `abc` and an overflowing number for a `long`). It should assert that the user's method is **not** invoked and that the response is a 400, 401 or deny as appropriate. Extend `AuthorizerBindingFailureTests.cs` for authorizers. +5. Remember that existing snapshot tests mostly use `string` parameters, which can't fail conversion. Snapshots alone don't prove the failure path works, so the runtime test in step 4 is required. + +## Change Files + +Every change needs an AutoVer change file (see `CONTRIBUTING.md`). Run from the repository root: +``` +autover change --project-name "Amazon.Lambda.Annotations" -m "" +```