Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom cookie authentication not working after migration from ASP.NET Core 1.1 MVC to 2.0

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://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x and some similar, but they didn't help me resolve the issue.

like image 668
Marko Avatar asked Jan 03 '18 08:01

Marko


Video Answer


1 Answers

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 :)

like image 97
MarkovskI Avatar answered Sep 22 '22 07:09

MarkovskI