6

I have migrated an ASP.NET Core 1.1 MVC project to ASP.NET Core 2.0 and now I note that requests to unauthorized sections of the application no longer result with a "401 Unauthorized" response but rather with a code exception leading to a response "500 internal server error".

An example excerpt from the log file (John Smith is not authorized to acces the controller action he tried to access):

2018-01-02 19:58:23 [DBG] Request successfully matched the route with name '"modules"' and template '"m/{ModuleName}"'.
2018-01-02 19:58:23 [DBG] Executing action "Team.Controllers.ModulesController.Index (Team)"
2018-01-02 19:58:23 [INF] Authorization failed for user: "John Smith".
2018-01-02 19:58:23 [INF] Authorization failed for the request at filter '"Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter"'.
2018-01-02 19:58:23 [INF] Executing ForbidResult with authentication schemes ([]).
2018-01-02 19:58:23 [INF] Executed action "Team.Controllers.ModulesController.Index (Team)" in 146.1146ms
2018-01-02 19:58:23 [DBG] System.InvalidOperationException occurred, checking if Entity Framework recorded this exception as resulting from a failed database operation.
2018-01-02 19:58:23 [DBG] Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services.
2018-01-02 19:58:23 [ERR] An unhandled exception has occurred while executing the request
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultForbidScheme found.
at Microsoft.AspNetCore.Authentication.AuthenticationService.<ForbidAsync>d__12.MoveNext()
...

I use a custom cookie authentication, implemented as a middleware. Here is my Startup.cs (app.UseTeamAuthentication() is the call to the middleware):

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<MyAppOptions>(Configuration);
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        services.AddDbContext<ApplicationDbContext>(options => options
            .ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning))
            .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)));

        services.AddAuthorization(options =>
        {
            options.AddPolicy(Security.TeamAdmin, policyBuilder => policyBuilder.RequireClaim(ClaimTypes.Role, Security.TeamAdmin));
            options.AddPolicy(Security.SuperAdmin, policyBuilder => policyBuilder.RequireClaim(ClaimTypes.Role, Security.SuperAdmin));
        });

        services.AddDistributedMemoryCache();
        services.AddSession(options =>
        {
            options.IdleTimeout = System.TimeSpan.FromMinutes(5);
            options.Cookie.HttpOnly = true;
        });

        services.AddMvc()
            .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
            .AddViewLocalization(
                LanguageViewLocationExpanderFormat.SubFolder,
                options => { options.ResourcesPath = "Resources"; })
            .AddDataAnnotationsLocalization();

        services.Configure<RequestLocalizationOptions>(options =>
        {
            options.DefaultRequestCulture = new RequestCulture("en-US");
            options.SupportedCultures = TeamConfig.SupportedCultures;
            options.SupportedUICultures = TeamConfig.SupportedCultures;
            options.RequestCultureProviders.Insert(0, new MyCultureProvider(options.DefaultRequestCulture));
        });

        services.AddScoped<IViewLists, ViewLists>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.File("log.txt", outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message}{NewLine}{Exception}")
            .CreateLogger();
        loggerFactory.AddSerilog();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }

        bool UseHttps = Configuration.GetValue("Https", false);
        if (UseHttps)
        {
            app.UseRewriter(new RewriteOptions().AddRedirectToHttps());
        }

        app.UseStaticFiles();

        app.UseTeamDatabaseSelector();
        app.UseTeamAuthentication();

        var localizationOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
        app.UseRequestLocalization(localizationOptions.Value);

        app.UseSession();
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "modules",
                template: "m/{ModuleName}",
                defaults: new { controller = "Modules", action = "Index" }
                );
            routes.MapRoute(
                name: "actions",
                template: "a/{action}",
                defaults: new { controller = "Actions" }
                );
            routes.MapRoute(
                name: "modules_ex",
                template: "mex/{action}",
                defaults: new { controller = "ModulesEx" }
                );
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Here is the middleware:

public class TeamAuthentication
{
    private readonly RequestDelegate next;
    private readonly ILogger<TeamAuthentication> logger;

    public TeamAuthentication(RequestDelegate _next, ILogger<TeamAuthentication> _logger)
    {
        next = _next;
        logger = _logger;
    }

    public async Task Invoke(HttpContext context, ApplicationDbContext db)
    {
        if (TeamConfig.AuthDebug)
        {
            logger.LogDebug("Auth-Invoke: " + context.Request.Path);
        }

        const string LoginPath = "/Login";
        const string LoginPathTimeout = "/Login?timeout";
        const string LogoutPath = "/Logout";

        bool Login =
            (context.Request.Path == LoginPath ||
            context.Request.Path == LoginPathTimeout);
        bool Logout = (context.Request.Path == LogoutPath);

        string TokenContent = context.Request.Cookies["t"];

        bool DatabaseSelected = context.Items["ConnectionString"] != null;
        bool Authenticated = false;
        bool SessionTimeout = false;

        // provjera tokena
        if (!Login && !Logout && DatabaseSelected && TokenContent != null)
        {
            try
            {
                var token = await Security.CheckToken(db, logger, TokenContent, context.Response);
                if (token.Status == Models.TokenStatus.OK)
                {
                    Authenticated = true;
                    context.Items["UserID"] = token.UserID;
                    List<Claim> userClaims = new List<Claim>();

                    var person = await db.Person.AsNoTracking()
                        .Where(x => x.UserID == token.UserID)
                        .FirstOrDefaultAsync();

                    if (person != null)
                    {
                        var emp = await db.Employee.AsNoTracking()
                            .Where(x => x.PersonID == person.ID)
                            .FirstOrDefaultAsync();
                        if (emp != null)
                        {
                            context.Items["EmployeeID"] = emp.ID;
                        }
                    }

                    string UserName = "";
                    if (person != null && person.FullName != null)
                    {
                        UserName = person.FullName;
                    }
                    else
                    {
                        var user = await db.User.AsNoTracking()
                            .Where(x => x.ID == token.UserID)
                            .Select(x => new { x.Login }).FirstOrDefaultAsync();
                        UserName = user.Login;
                    }
                    context.Items["UserName"] = UserName;
                    userClaims.Add(new Claim(ClaimTypes.Name, UserName));

                    if ((token.Roles & (int)Security.TeamRoles.TeamAdmin) == (int)Security.TeamRoles.TeamAdmin)
                    {
                        userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin));
                    }

                    if ((token.Roles & (int)Security.TeamRoles.SuperAdmin) == (int)Security.TeamRoles.SuperAdmin)
                    {
                        userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin));
                        userClaims.Add(new Claim(ClaimTypes.Role, Security.SuperAdmin));
                    }

                    ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims, "local"));
                    context.User = principal;
                }
                else if (token.Status == Models.TokenStatus.Expired)
                {
                    SessionTimeout = true;
                }
            }
            catch (System.Exception ex)
            {
                logger.LogCritical(ex.Message);
            }
        }

        if (Login || (Logout && DatabaseSelected) || Authenticated)
        {
            await next.Invoke(context);
        }
        else
        {
            if (Utility.IsAjaxRequest(context.Request))
            {
                if (TeamConfig.AuthDebug)
                {
                    logger.LogDebug("Auth-Invoke => AJAX 401");
                }
                context.Response.StatusCode = 401;
                context.Response.Headers.Add(SessionTimeout ? "X-Team-Timeout" : "X-Team-Login", "1");
            }
                else
                {
                    string RedirectPath = SessionTimeout ? LoginPathTimeout : LoginPath;
                    if (TeamConfig.AuthDebug)
                    {
                        logger.LogDebug("Auth-Invoke => " + RedirectPath);
                    }
                    context.Response.Redirect(RedirectPath);
                }
            }
        }
    }
}

Here is the same middleware, with the code that I believe is not important for the question stripped out:

public class TeamAuthentication
{
    private readonly RequestDelegate next;
    private readonly ILogger<TeamAuthentication> logger;

    public async Task Invoke(HttpContext context, ApplicationDbContext db)
    {
        // preparatory actions...

        var token = await Security.CheckToken(db, logger, TokenContent, context.Response);
        if (token.Status == Models.TokenStatus.OK)
        {
            List<Claim> userClaims = new List<Claim>();
            string UserName = "";

            // find out the UserName...

            userClaims.Add(new Claim(ClaimTypes.Name, UserName));

            if ((token.Roles & (int)Security.TeamRoles.TeamAdmin) == (int)Security.TeamRoles.TeamAdmin)
            {
                userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin));
            }

            if ((token.Roles & (int)Security.TeamRoles.SuperAdmin) == (int)Security.TeamRoles.SuperAdmin)
            {
                userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin));
                userClaims.Add(new Claim(ClaimTypes.Role, Security.SuperAdmin));
            }

            ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims, "local"));
        }

        // ...

This is how I authorize access to the controller:

namespace Team.Controllers
{
    [Authorize(Policy = Security.TeamAdmin)]
    public class ModulesController : Controller
    {
        // ...

I tried to research the issue by Google-ing and found articles like https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x and some similar, but they didn't help me resolve the issue.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Marko
  • 1,502
  • 5
  • 21
  • 40
  • Do your project have reference to Microsoft.AspNetCore.Server.IISIntegration ? Get the latest one for .NET Core 2.0 from Nuget – KiKMak Jan 12 '18 at 15:13
  • What would I need this reference for? Were there a missing dependancy, it'd result in a compile error. – Marko Jan 15 '18 at 07:58

1 Answers1

2

IMHO you might want to switch to the built in Role base authorization instead of rolling your own custom policy authorization there are bound to be cases you haven't thought of that are handled by it (avoid reinventing the wheel :).

For authentication you should set up the cookie authentication scheme using

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();

Read about the settings it provides here, for a custom scheme without ASP.Net Identity.

As for authorization you have mixed authentication and authorization a bit here, the middleware does both but is named UseTeamAuthentication the difference is explained here, and as such these two things are separate in the ASP.Net Core infrastructure.

Authorization as you have done it (custom) needs to be done by implementing requirements through the IAuthorizationRequirement interface, you can read how to do that in the above custom policy link. But I strongly suggest you use the built in Roles mechanism.

Cheers :)

MarkovskI
  • 1,489
  • 2
  • 21
  • 25
  • The reason I wrote a custom cookie authentication is to have something that will work no matter what change Microsoft introduces with a newer version of .NET Core. It's fully under my control and I can resolve any issue and change it's behavior. The middleware does assign policies, but as I see "autorize" filter as the authorization point, I think that the middleware name still matches good enough it's purpose. I did read all the linked documentation before wtiting my security-related code. However I still miss the clue why code that works fine with one version of .NET Core fails with another. – Marko Jan 08 '18 at 12:20
  • 2
    Fair enough :), but the thing is most of the security architecture was changed from v1 to v2 and as you've seen your code does depend on the underlying asp.net Core architecture so even if you have decoupled from the security (which you haven't as you do use authorization and policies) you still depend on it and changes there do influence your code, so don't expect things to continue working. For your specific case I think this will guide you in the right direction: https://github.com/aspnet/Security/issues/1219 – MarkovskI Jan 08 '18 at 12:45