40

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");
        }
    }
}
10
  • Can you show the method and classes where you applied the authorize attribute? Commented Nov 17, 2016 at 5:11
  • Where's your JWT token middleware? Your service is properly generating a JWT through the /token end point or something similar? Commented Nov 17, 2016 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. Commented Nov 17, 2016 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. Commented Nov 17, 2016 at 19:31
  • Yeah, I just tried adding them in and no difference. There must be something I'm missing... Commented Nov 17, 2016 at 19:33

7 Answers 7

61

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());
Sign up to request clarification or add additional context in comments.

5 Comments

This caught me out as well (since I'm using AddMvcCore). The call to AddAuthorization() adds the AuthorizationApplicationModelProvider which looks for Authorize/AllowAnonymous on controllers and adds the appropriate policies.
Don't forget AddDataAnnotations too, if you use them for request validation!
Authorize attribute started working after using services.AddMvcCore().AddAuthorization()
I think you can check this answer stackoverflow.com/a/63446357/4307338
@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
42

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(AuthenticationSchemes = "Bearer")]

10 Comments

I will try this when I get back to work tomorrow, I hope you're right. On that note, the reason I am using identity is purely to hide/abstract the password hashing. What alternative would you suggest?
Identity is recommended solution. If you need then go with it. As far as i understand you want to use identity and jwt optional. In this case you should specify authentication scheme to restrict. For example you should use bearer scheme if you want only jwt authentication.
You don't need the ActiveAuthenticationSchemes to use JWT. Works without it as long as you send the token properly.
@SledgeHammer sure, normally you don't need it. But i assumed the problem caused by identity authentication and also assumed OP wants to jwt authentication only.
You are my hero
|
25

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")]

2 Comments

I think you can check this answer stackoverflow.com/a/63446357/4307338
No problem, bud. Happy coding.
4

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 .

7 Comments

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
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
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
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
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
|
4

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.

Comments

2

If you are using a custom scheme, you must use

[Authorize(AuthenticationSchemes="your custom scheme")]

Comments

0

If you don't want to use:

[Authorize(AuthenticationSchemes="your custom scheme")]

In your project add:

AppContext.SetSwitch("Microsoft.AspNetCore.Authentication.SuppressAutoDefaultScheme", false);

See https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/7.0/default-authentication-scheme for more detail. It's a breaking change from .NET 7.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.