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.