37

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");
        }
    }
}
Fred
  • 2,965
  • 4
  • 29
  • 51
Andrew Williamson
  • 6,465
  • 2
  • 30
  • 48
  • Can you show the method and classes where you applied the authorize attribute? – jegtugado Nov 17 '16 at 05:11
  • Where's your JWT token middleware? Your service is properly generating a JWT through the /token end point or something similar? – SledgeHammer Nov 17 '16 at 19:22
  • At this point, I don't care about generating tokens, I just care about rejecting anything that doesn't provide a token in the headers. – Andrew Williamson Nov 17 '16 at 19:23
  • What you have now is pretty identical to what I have, except I have ValidateIssuer = true and ValidateAudience = true, but I don't think those are required. I don't have services.AddAuthentication(); either. – SledgeHammer Nov 17 '16 at 19:31
  • Yeah, I just tried adding them in and no difference. There must be *something* I'm missing... – Andrew Williamson Nov 17 '16 at 19:33
  • I tried my service with OUT my token middleware and it gets a 401 with no token. Also tried with app.UseAuthentication(). – SledgeHammer Nov 17 '16 at 19:36
  • Mine returns: { "date": "Thu, 17 Nov 2016 19:37:11 GMT", "www-authenticate": "Bearer", "x-sourcefiles": "", "server": "Kestrel", "x-powered-by": "ASP.NET", "content-length": "0", "content-type": null } – SledgeHammer Nov 17 '16 at 19:38
  • One thing I noticed thats different is that you didn't decorate your Get() method with [HttpGet]. It *MIGHT* have something to do with that as it'll treat the method differently without the attribute... worth trying :). – SledgeHammer Nov 17 '16 at 19:39
  • Adding `[HttpGet]` had no result. By the way, I've based my project off [the sample program](https://github.com/aspnet/Security/blob/dev/samples/JwtBearerSample) on their website, but it doesn't use controllers or the Authorize attribute. – Andrew Williamson Nov 17 '16 at 19:54
  • I think you can check this answer https://stackoverflow.com/a/63446357/4307338 – Hoque MD Zahidul Aug 17 '20 at 08:41

6 Answers6

56

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());
Andrew Williamson
  • 6,465
  • 2
  • 30
  • 48
  • 6
    This caught me out as well (since I'm using `AddMvcCore`). The call to `AddAuthorization()` adds the [`AuthorizationApplicationModelProvider`](https://github.com/aspnet/Mvc/blob/760c8f38678118734399c58c2dac981ea6e47046/src/Microsoft.AspNetCore.Mvc.Core/Internal/AuthorizationApplicationModelProvider.cs) which looks for Authorize/AllowAnonymous on controllers and adds the appropriate policies. – Ben Foster Mar 23 '17 at 10:01
  • 1
    Don't forget `AddDataAnnotations` too, if you use them for request validation! – Peter Morris Nov 13 '17 at 09:27
  • Authorize attribute started working after using `services.AddMvcCore().AddAuthorization()` – user2167322 Jul 26 '20 at 17:34
  • I think you can check this answer https://stackoverflow.com/a/63446357/4307338 – Hoque MD Zahidul Aug 17 '20 at 08:42
  • @HoqueMDZahidul The aim was to produce a minimal example using `MvcCore`. The answer you have linked to shows a working asp net configuration, but it does not show _the bare minimum configuration to get token authentication working_. If all I wanted was just for authentication to work, I could have just kept using `.AddMvc()` instead of `.AddMvcCore()`. Your answer does not address the original question – Andrew Williamson Aug 17 '20 at 20:22
31

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")]
adem caglin
  • 17,749
  • 8
  • 44
  • 67
18

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")]
Andre Mendonca
  • 516
  • 5
  • 11
2

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 .

  • One of the key points in the question says `I don't need (and don't want) to include ASP.Net Identity`. It's easy enough to set up Asp.Net with a standard configuration, but the goal was to create a minimal project that uses MvcCore and JwtAuthentication – Andrew Williamson Mar 07 '20 at 00:47
  • The answer covers both types of authentication mechanisms you can skip anyone of them as per your requirement.you just need to comment out the part that you don't need @AndrewWilliamson – Tushar Kshirsagar Mar 10 '20 at 06:23
  • It's not just an issue of commenting out a part that isn't needed. Your `ConfigureServices` method calls `.AddMvc()` instead of `.AddMvcCore()`. My whole question was based around authentication not working when I use `.AddMvcCore()`, and I had to read through the source code for `.AddMvc()` in order to figure out what the difference was. When you provide an answer, it should include the _minimal_ amount of changes in order to answer the question. Otherwise users still have to work out which part of your code solves the problem – Andrew Williamson Mar 10 '20 at 22:24
  • my solution indeed a great experience that I had while working and I thought if someone need just a clue for the how dual authentication works it could save the day. As you have already down voted I do understand that its not worth for your case. – Tushar Kshirsagar Mar 12 '20 at 06:57
  • 1
    I appreciate you wanting to share your experience with others - don't stop, just remember to focus on answering the question with a minimal example – Andrew Williamson Mar 12 '20 at 08:08
  • 1
    Thanks for `[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]` attribute. In my case, for WebApi I want to use JWTAuth, and for ViewController I want to use CookieAuth. Then I've created double AddAuthentication(). But unfortunately, since I am using `[Authorize]` without adding parameter. the Unauthorized page always hit JWTAuth Settings. But after adding parameter on `[Authorize]` this solve my problem – Fadhly Permata Jul 24 '20 at 07:53
  • I think you can check this answer https://stackoverflow.com/a/63446357/4307338 – Hoque MD Zahidul Aug 17 '20 at 08:41
2

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.

Joe Mayo
  • 7,282
  • 7
  • 37
  • 56
Danich
  • 65
  • 7
0

If you are using a custom scheme, you must use

[Authorize(AuthenticationSchemes="your custom scheme")]
Leandro
  • 6,537
  • 12
  • 59
  • 95