Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Authorize attribute not working with JWT

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");
        }
    }
}
like image 514
Andrew Williamson Avatar asked Nov 17 '16 03:11

Andrew Williamson


6 Answers

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());
like image 189
Andrew Williamson Avatar answered Oct 24 '22 01:10

Andrew Williamson


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")]
like image 20
adem caglin Avatar answered Oct 24 '22 01:10

adem caglin


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")]
like image 26
André Mendonça Avatar answered Oct 24 '22 03:10

André Mendonça


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 .

like image 3
Tushar Kshirsagar Avatar answered Oct 24 '22 02:10

Tushar Kshirsagar


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.

like image 3
Danich Avatar answered Oct 24 '22 01:10

Danich


If you are using a custom scheme, you must use

[Authorize(AuthenticationSchemes="your custom scheme")]
like image 1
Leandro Bardelli Avatar answered Oct 24 '22 03:10

Leandro Bardelli