I want to implement JWT-based security in ASP.Net Core. All I want it to do, for now, is to read bearer tokens in the Authorization
header and validate them against my criteria. I don't need (and don't want) to include ASP.Net Identity. In fact, I'm trying to avoid using as many of the things that MVC adds in as possible unless I really need them.
I've created a minimal project, which demonstrates the problem. To see the original code, just look through the edit history. I was expecting this sample to reject all requests for /api/icons
unless they provide the Authorization
HTTP header with a corresponding bearer token. The sample actually allows all requests.
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Routing;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System;
using Newtonsoft.Json.Serialization;
namespace JWTSecurity
{
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(env.ContentRootPath);
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.AddAuthentication();
services.AddMvcCore().AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("supersecretkey")),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}
});
app.UseMvc(routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"));
}
}
}
Controllers/IconsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JWTSecurity.Controllers
{
[Route("api/[controller]")]
public class IconsController : Controller
{
[Authorize]
public IActionResult Get()
{
return Ok("Some content");
}
}
}
Found it!
The main problem is in this line:
services.AddMvcCore().AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());
I noticed that by switching from AddMvcCore() to AddMvc(), the authorization suddenly started working! After digging through the ASP.NET source code, to see what AddMvc()
does, I realized that I need a second call, to IMvcBuilder.AddAuthorization()
.
services.AddMvcCore()
.AddAuthorization() // Note - this is on the IMvcBuilder, not the service collection
.AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());
You are also using identity authentication and it contains cookie authentication implicitly. Probably you logged in with identity scheme and it caused successful authentication.
Remove identity authentication if it is not required(if you want only jwt authentication), otherwise specify Bearer
scheme for Authorize
attribute like below:
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
For those who even tried the previews answers and did not get the problem solved, below it is how the problem was solved in my case.
[Authorize(AuthenticationSchemes="Bearer")]
Found the perfect solution to this problem Your configure services class should look like below
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>
(options => options.Stores.MaxLengthForKeys = 128)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
//options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
//options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
//options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(cfg => cfg.SlidingExpiration = true)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
});
//Seting the Account Login page
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/Account/Login"; // If the LoginPath is not set here, ASP.NET Core will default to /Account/Login
options.LogoutPath = "/Account/Logout"; // If the LogoutPath is not set here, ASP.NET Core will default to /Account/Logout
options.AccessDeniedPath = "/Account/AccessDenied"; // If the AccessDeniedPath is not set here, ASP.NET Core will default to /Account/AccessDenied
options.SlidingExpiration = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
you can authenticate Web API Controller like below
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class TaskerController : ControllerBase
{
[HttpGet("[action]")]
//[AllowAnonymous]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
and You can use Identity based Authorize attribute as usual like below for MVC controller
public class TaskController : Controller
{
[Authorize]
public IActionResult Create()
{
}
}
Key solution is .AddCookie(cfg => cfg.SlidingExpiration = true)
adding before JWT authentication i.e .AddJwtBearer(//removed for brevity)
sets Cookie based authorization as default and so [Authorize] works as usual and whenever you need JWT you have to invoke it explicitly using [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Hope it will help someone who wants Website as front end and clubbing mobile ready Web API as back end .
I just had a similar problem, and turns out that [AllowAnonymous]
attribute at controller level overrides any [Authorize]
attributes applied to any action within that controller. This is something I didn't know before.
If you are using a custom scheme, you must use
[Authorize(AuthenticationSchemes="your custom scheme")]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With