Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS Lambda Annotation Functions never passes through middleware #1875

Open
1 task
BryZeNtZa opened this issue Nov 18, 2024 · 4 comments
Open
1 task

AWS Lambda Annotation Functions never passes through middleware #1875

BryZeNtZa opened this issue Nov 18, 2024 · 4 comments
Labels
annotations feature-request A feature should be added or improved. p2 This is a standard priority issue queued

Comments

@BryZeNtZa
Copy link

Describe the bug

I'm writing C# Lambda functions to be exposed by the API Gateway service, using the AWS Annotation Framework.

I successfully registered application services in the ConfigureServices(IServiceCollection services) method of the Startup.cs file.

In order to add some API configurations in the header of all incoming requests (Authorization header, etc.), I registered a middleware via the Configure(IApplicationBuilder app, IWebHostEnvironment env) method of the the Startup.cs file.

The problem is, the application is completely ignoring the middleware; in other terms, the application never passes through the middleware.

Here is the code:

Lambda function (in Function.cs file):

using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;
using TbTypes.Model.Api.Requests;
using Microsoft.AspNetCore.Mvc;
using FromServicesAttribute = Amazon.Lambda.Annotations.FromServicesAttribute;
using FromBodyAttribute = Microsoft.AspNetCore.Mvc.FromBodyAttribute;
using TbTypes.Services.Thingsboard;
using TbTypes.Model.Api.Reponses;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace TbInterface;

public class Function
{
    /// <summary>
    /// Function for requesting TB Devices visible by a User
    /// </summary>
    [LambdaFunction()]
    [HttpGet("/user/devices/")]
    public async Task<DevicesPage> FetchDevices(
        [FromBody] UserDevicesRequestBody body,
        ILambdaContext context,
        [FromServices] IDeviceService service)
    {
       // you can replace IDeviceService by a dummy service when reproducing the issue
        return await service.FetchDevices(body.claims.TbUserID, body.claims.CognitoUserID);
    }
}

My Startups.cs file with services registration in ConfigureServices() and middleware registration in Configure() method:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using TbInterface.Configuration;
using TbInterface.Middlewares;
using TbInterface.Repositories;
using TbInterface.Services.Cache;
using TbInterface.Services.Database;
using TbInterface.Services.Thingsboard;
using TbTypes.Configuration;
using TbTypes.Repositories;
using TbTypes.Services.Cache;
using TbTypes.Services.Thingsboard;

namespace TbInterface
{
    [LambdaStartup]
    public class Startup
    {
        private IConfiguration Configuration;

        public Startup()
        {
            Configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddEnvironmentVariables()
            .Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {

            // Application Configurations
            services.AddSingleton<IConfiguration>(implementationInstance: Configuration);
            services.AddSingleton<ITbConfiguration>(sp => {
                var settings = sp.GetRequiredService<IConfiguration>();
                return new TbConfiguration(settings);
            });

            // Cache Service: AWS Elasti Cache
            services.AddSingleton<IElastiCacheService, ElastiCacheService>();

            // Database Service: AWS DynamoDB
            services.AddSingleton<IAmazonDynamoDB, DynamoDB>();
            services.AddAWSService<IAmazonDynamoDB>();
            //services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
            services.AddAWSService<Amazon.S3.IAmazonS3>();
            services.AddAWSService<IAmazonDynamoDB>();


            // Repositories
            services.AddSingleton<IUserTokenRepository, UserTokenRepository>();

            // Thingsboard API services
            services.AddSingleton<IAuthTokenService, AuthTokenService>();
            services.AddSingleton<IUserService, UserService>();
            services.AddSingleton<IDeviceService, DeviceService>();
            services.AddTransient<TbApiConfigurationMiddleware>();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // Here's how I resgistered the middleware
            app.UseMiddleware<TbApiConfigurationMiddleware>();
        }

    }
}

The middleware itself - TbApiConfigurationMiddleware.cs :

using Microsoft.AspNetCore.Http;
using System.Net;
using Newtonsoft.Json;
using TbAPIClient.Generated.Client;
using TbAPIClient.Generated.Model;
using TbClientConfiguration = TbAPIClient.Generated.Client.Configuration;
using TbTypes.Model.Api.Requests;
using TbTypes.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace TbInterface.Middlewares
{
    public class TbApiConfigurationMiddleware : IMiddleware
    {
        /// <summary>
        /// Custom logic to be executed before the next middleware
        /// </summary>
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            var authService = context.RequestServices.GetService<IAuthTokenService>();

            BaseApiRequestBody? body = extractRequestBody(context);

            if (body == null || body.claims == null) {
                await handleBadRequestBody(context, "Bad request body format");
                return;
            }

            //JwtPair? token = await _authService.RetrieveOrRequestUserToken(body.claims.TbUserID!);
            JwtPair? token = await authService!.FetchUserTokenAsync(body.claims.TbUserID!);

            if (token == null)
            {
                await handleUnauthorizedUser(context);
                return;
            }

            var tbConfiguration = context.RequestServices.GetService<ITbConfiguration>();
            ConfigureTbApiToken(token!, tbConfiguration!);

            await next(context);
        }

        /// <summary>
        /// Extract request body to perform basic format validation
        /// </summary>
        /// <param name="context">HTTP Context</param>
        /// <returns></returns>
        private BaseApiRequestBody? extractRequestBody(HttpContext context) {
            var rawBody = context.Request.Body.ToString();

            if (rawBody == null)
            {
                return null;
            }

            return JsonConvert.DeserializeObject<BaseApiRequestBody>(rawBody);
        }

        /// <summary>
        /// Handling bad request body
        /// </summary>
        /// <param name="context">HTTP Context</param>
        /// <returns></returns>
        private async Task handleBadRequestBody(HttpContext context, string message) {
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            context.Response.ContentType = "application/json";
            var body = new ApiException(context.Response.StatusCode, message);
            await context.Response.WriteAsync(JsonConvert.SerializeObject(body));
        }


        /// <summary>
        /// Configuring middleware response in case of Unauthorized User
        /// </summary>
        /// <param name="context">HTTP Context</param>
        /// <returns></returns>
        private async Task handleUnauthorizedUser(HttpContext context)
        {
            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            context.Response.ContentType = "application/json";
            var body = new ApiException(context.Response.StatusCode, "Unauthorized user");
            await context.Response.WriteAsync(JsonConvert.SerializeObject(body));
        }

        /// <summary>
        /// Method for configuring Thingsboard API Auth Token
        /// </summary>
        /// <param name="token">Token definition: {Token, RefreshToken} </param>
        /// <param name="tbConfiguration">Application configs </param>
        /// <returns></returns>
        private void ConfigureTbApiToken(JwtPair token, ITbConfiguration tbConfiguration)
        {
            TbClientConfiguration.Default.ApiKeyPrefix[tbConfiguration.TokenHeaderKey] = tbConfiguration.TokenType;
            TbClientConfiguration.Default.ApiKey[tbConfiguration.TokenHeaderKey] = (string)token.Token;
        }
    }
}

Regression Issue

  • Select this option if this issue appears to be a regression.

Expected Behavior

I want my API Config Middleware (here TbApiConfigurationMiddleware) to add some API configurations in the header of all incoming requests (Authorization header, etc.).
At least, I want to see that the application is passing through the middleware when a request enters.

Current Behavior

The application never passes through the middleware when a request comes in.

Reproduction Steps

Use Amazon Lambda Annotations Framework guidelines and:

1- Create a serverless Lambda Functions project with the AWS Lambda Annotations Framework with Visual Studio
2- Register a middleware that add a header in the request (whatever header name you want)
3- Create a Lambda function that take an HTTP request and output the header created in the middleware
4- Check that the application passes through the middleware and that the application outputs the header added by the middleware

Possible Solution

No response

Additional Information/Context

I use Visual Studio Community 2022

AWS .NET SDK and/or Package version used

AWSSDK.Core 3.7.400.45
AWSSDK.S3 3.7.405.9

Targeted .NET Platform

.NET 8

Operating System and version

Windows 11

@BryZeNtZa BryZeNtZa added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Nov 18, 2024
@ashishdhingra ashishdhingra added needs-reproduction This issue needs reproduction. annotations and removed needs-triage This issue or PR still needs to be triaged. labels Nov 18, 2024
@ashishdhingra
Copy link
Contributor

Needs reproduction

@ashishdhingra
Copy link
Contributor

@BryZeNtZa Good afternoon. Please refer to #1688 (comment) for similar discussion started a while ago. While on high level it might appear to have some kind of relation to ASP.NET framework:

  • Lambda annotations relies on source generators to translate your annotated Lambda functions to the regular Lambda programming model (refer https://aws.amazon.com/blogs/developer/net-lambda-annotations-framework/).
  • Decorating a class with Amazon.Lambda.Annotations.LambdaStartup attribute causes source generation process to invoke <<class>>.ConfigureServices() in the generated code for LambdaFunctions. Below is an example:
...
namespace SomeNamespace
{
    public class Functions_Add_Generated
    {
        private readonly ServiceProvider serviceProvider;

        public Functions_Add_Generated()
        {
            SetExecutionEnvironment();
            var services = new ServiceCollection();

            ...
            var startup = new LambdaAnnotations_Issue1368.Startup();
            startup.ConfigureServices(services);
            serviceProvider = services.BuildServiceProvider();
        }

        public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse Add(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__)
        {
...
    }
}
  • Source generation process also generates serverless.template to create API Gateway resources. So it's mapping HTTP API endpoints to generated Lambda functions.

Hence, there is no concept of ASP.NET's Request object which might have given access to raw HTTP request and query string parameters.

Hence, middleware is not invoked while using annotations.

Reference example (using both programming models): https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/dotnetv3/cross-service/PhotoAssetManager

CCing @normj for any other inputs.

Thanks,
Ashish

@ashishdhingra ashishdhingra added response-requested Waiting on additional info and feedback. Will move to close soon in 7 days. p2 This is a standard priority issue and removed needs-reproduction This issue needs reproduction. labels Nov 18, 2024
@BryZeNtZa
Copy link
Author

Thanks for your answer @ashishdhingra .

However Middleware pattern is a so useful feature in Web development nowadays.

AWS AF would definitely find a way to provide it.

Thanks.

@ashishdhingra
Copy link
Contributor

Thanks for your answer @ashishdhingra .

However Middleware pattern is a so useful feature in Web development nowadays.

AWS AF would definitely find a way to provide it.

Thanks.

@BryZeNtZa I will review this with the team if supporting middleware somehow is feasible given that Annotations framework doesn't leverage ASP.NET pipeline.

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to close soon in 7 days. label Nov 20, 2024
@ashishdhingra ashishdhingra added feature-request A feature should be added or improved. queued and removed bug This issue is a bug. needs-review labels Dec 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
annotations feature-request A feature should be added or improved. p2 This is a standard priority issue queued
Projects
None yet
Development

No branches or pull requests

2 participants