Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Middleware Ordering

I have a new .NET Core 3.1 app and am struggling with the concept of Middleware. From reading around, it seems the order of including different middlewares is important. I currently have several problems which I can't seem to solve:

  1. I never see the developer error page, and have to check the event log to see what's happened if there's an error. I just get the blank "error 500" etc pages from Chrome. The custom error pages also never display when there's a 500/400.
  2. The app always tries to redirect me to /Account/Login despite changing this in the cookie settings.
  3. User.IsAuthenticated returns false when the CheckPermissionsAction call is made in Elmah, so I can't access Elmah. The User.IsInRole call works for from controllers though.

This is how I'm bootstrapping the app. It feels like something is overriding the settings:

public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<DataProtectionTokenProviderOptions>(options =>
            options.TokenLifespan = TimeSpan.FromDays(2));

        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromMinutes(30);
        });
        services.AddControllersWithViews();
        services.AddTransient<IUserStore<User>, UserStore>();
        services.AddTransient<IRoleStore<IdentityRole>, RoleStore>();
        services.AddRazorPages();
        

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.LoginPath = new PathString("/login");
                options.AccessDeniedPath = new PathString("/error/denied");
                options.LogoutPath = new PathString("/log-off");
                options.ExpireTimeSpan = TimeSpan.FromDays(60);
                options.SlidingExpiration = true;
                options.Cookie.HttpOnly = true;
                options.Cookie.Name = "MyCookie";
                options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
                options.Cookie.SameSite = SameSiteMode.Lax;
            });
        services.AddIdentity<User, IdentityRole>(options =>
        {
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 6;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = false;
            options.Password.RequireLowercase = false;
        })
        .AddUserStore<UserStore>()
        .AddRoleStore<RoleStore>()
        .AddDefaultTokenProviders();
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => false;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddElmah<SqlErrorLog>(options =>
        {
            options.ConnectionString = Configuration.GetConnectionString("MyApp");
            options.CheckPermissionAction = (context)=>{
                return context.User.Identity.IsAuthenticated && context.User.IsInRole(RoleHelper.SuperAdmin);
            };
            options.Path = "/elmah";
        });
        services.AddSingleton<IAppConfiguration, AppConfiguration>(e => Configuration.GetSection("AppConfig")
           .Get<AppConfiguration>());

        OptionsConfigurationServiceCollectionExtensions.Configure<DbHelper>(services, Configuration.GetSection("ConnectionStrings"));
        services.AddHttpContextAccessor();
    }

    public void ConfigureContainer(ContainerBuilder builder)
    {
        // wire up using autofac specific APIs here
        builder.Register(context => new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<User, MyDetailsViewModel>();
        })).AsSelf().SingleInstance();
        builder.RegisterModule(new RegistrationModule()); // separate assembly, wires up autofac registrations
        builder.Register(c =>
        {
            //This resolves a new context that can be used later.
            var context = c.Resolve<IComponentContext>();
            var config = context.Resolve<MapperConfiguration>();
            return config.CreateMapper(context.Resolve);
        })
        .As<IMapper>()
        .InstancePerLifetimeScope();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            // debugger shows this section is called, but I never see the error page.
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
            app.UseRouteDebugger();
        }
        else
        {
            app.UseExceptionHandler("/error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseSession();
        app.UseElmah();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        var cookiePolicyOptions = new CookiePolicyOptions
        {
            Secure = CookieSecurePolicy.SameAsRequest,
            MinimumSameSitePolicy = SameSiteMode.None
        };
        app.UseCookiePolicy(cookiePolicyOptions);
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Guest}/{action=Index}/{id?}");
            endpoints.MapRazorPages();
            endpoints.MapControllers();
        });

        app.UseStatusCodePages(async ctx =>
        {
            //Re-execute the request so the user gets the error page
            string originalPath = ctx.HttpContext.Request.Path.Value;
            switch (ctx.HttpContext.Response.StatusCode)
            {
                case 401:
                    //Re-execute the request so the user gets the error page
                    ctx.HttpContext.Items["originalPath"] = originalPath;
                    ctx.HttpContext.Request.Path = "/error/denied";
                    break;
                case 412:
                    ctx.HttpContext.Items["originalPath"] = originalPath;
                    ctx.HttpContext.Request.Path = "/error/expired-account";
                    break;
                case 404:
                    ctx.HttpContext.Items["originalPath"] = originalPath;
                    ctx.HttpContext.Request.Path = "/error/not-found";
                    break;
                case 500:
                    ctx.HttpContext.Items["originalPath"] = originalPath;
                    ctx.HttpContext.Request.Path = "/error/not-found";
                    break;
            }
        });
        DapperExtensions.DapperExtensions.SetMappingAssemblies(new[]
            {
                 Assembly.GetAssembly(typeof(MyApp.Domain.Model.Note)),
                 Assembly.GetExecutingAssembly()
        });
    }
like image 990
Echilon Avatar asked May 16 '26 11:05

Echilon


1 Answers

In regards to the order of your middleware, there is a problem with it.

There is a section in the Microsoft docs dedicated to the order of middleware, I suggest reading it.

As for your middleware, the correct order would be:

        app.UseHttpsRedirection();
        app.UseStatusCodePages(async ctx =>
        {
            // Omitted for brevity.
        });

        app.UseStaticFiles();

        var cookiePolicyOptions = new CookiePolicyOptions
        {
            // Omitted for brevity.
        };
        app.UseCookiePolicy(cookiePolicyOptions);

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        // If the app uses session state, call Session Middleware after Cookie
        // Policy Middleware and before MVC Middleware.
        app.UseSession();

        app.UseElmah(); // Not sure about this one. I don't know what it's supposed to do?

        app.UseEndpoints(endpoints =>
        {
            // Omitted for brevity.
        });

        DapperExtensions.DapperExtensions.SetMappingAssemblies(new[]
        {
            // Omitted for brevity.
        });
like image 95
Dennis VW Avatar answered May 19 '26 00:05

Dennis VW