Ken Bonny
Ken Bonny

Reputation: 789

ASP.NET Core Middleware executes before authentication

As part of the login flow, I need to check that a user has access to the application I'm writing. I do this by querying a legacy db with the user id. Afterwards, I want to add the legacy id to the request so I don't have to look it up each time. So I build a policy that retrieves the legacy id of the logged in user and adds it to the request. Then I wrote a LoggingMiddleware to add that legacy id as a log property.

Both work nicely, but are executed in the wrong order. First the middleware is executed and then the authentication policy. So I'm constantly logging 'Anonymous' instead of the id I want to log. My Startup class has the correct order (I think), so I don't understand why it won't work correctly.

Startup.cs:

    public virtual void ConfigureServices(IServiceCollection services)
    {
      var authenticationSettings = Configuration.GetSection(ConfigurationConstants.JwtSectionName).Get<AuthenticationSettings>();
      services
        .AddAuthorization(options =>
          options.AddPolicy(SecurityConstants.PolicyName,
            new AuthorizationPolicyBuilder()
              .RequireAuthenticatedUser()
              .Build()))
        .AddScoped<ISecurityQueries, SecurityQueries>()
        .AddScoped<IAuthorizationHandler, VerifyIdPolicy>()
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
          options.Authority = authenticationSettings.Authority;
          options.TokenValidationParameters = new TokenValidationParameters
          {
            ValidateAudience = false,
            ValidateLifetime = true,
            ValidateIssuer = true,
            ValidIssuers = authenticationSettings.Issuers.Split(';')
          };
        });

      services
        .ConfigureMvc<ContractDomainException>(Configuration)
        .ConfigureRefitClient<IAppConfigApi>(Configuration, DataContextTypes.AppConfig)
        .ConfigureServices()
        .ConfigureSwagger("Contract HTTP API")
        .ConfigureValidation(new Dictionary<string, string> {{Assembly.GetExecutingAssembly().FullName, DataContextTypes.Contract}});
    }

    public virtual void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
      app
        .UseAuthentication()
        .UseCors()
        .UseMiddleware<LoggingMiddlewareTest>()
        .UseResponseCompression()
        .UseMvc();
    }

Controller.cs:

  [ApiController]
  [ApiVersion("1.0")]
  [Authorize(Policy = SecurityConstants.PolicyName)]
  public class ContractController : ControllerBase

Upvotes: 4

Views: 7417

Answers (1)

Ken Bonny
Ken Bonny

Reputation: 789

The answer was actually fairly simple. I use a piece of middleware that I register before the LoggingMiddlewareTest.

    public virtual void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
      app
        .UseAuthentication()
        .UseCors()
        .UseEndpointRouting() // new
        .UseMiddleware<FetchTechnicalUserMiddleware>() // new
        .UseMiddleware<LoggingMiddlewareTest>()
        .UseResponseCompression()
        .UseMvc();
    }

In this scenario, I don't need an AuthorizationHandler<>. I just need to make sure the FetchTechnicalUserMiddleware is executed before the LoggingMiddlewareTest. This is done by placing the fetch before the logging as they are executed in order.

The other thing that I needed to do was add the UseEndpointRouting setup. This allows me to access the endpoint metadata in my middleware. I add a specific AuthorizeAttribute to endpoints on which I need to fetch the technical user id. This allows me to do the following in my middleware:

    public async Task InvokeAsync(HttpContext context)
    {
        var authorization = context.Features.Get<IEndpointFeature>()?.Endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
        if (authorization is PersonAuthorizeAttribute)
        {
          // fetch technical user id
        }
    }

Without the UseEndpointRouting, the authorization variable would always be null. The reason I do this is to not hit the db unnecessarily and improve performance on calls that don't need this data.

A big thanks to Kirk Larkin for pointing me in the right direction.

Upvotes: 4

Related Questions