0

I have a .NET Core (Was 2.0, now step by step upgraded to 5) web application using MVC and standard Identity. It has a web based login/backend UI. The upgrade process has worked fine for this and all is operating as it should.

However, I also have a set of WebAPI controllers which I have using a JWT Bearer Token - and these have stopped working, and now all throw a 401 error.

I am pretty sure I need to somehow register the additional authorization scheme, but I am not sure how to do it.

Here is how the controller is annotation

    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [Route("api/[controller]")]

And here is the excerpt from my Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("TechsportiseDB")));
            //options.UseInMemoryDatabase("Techsportise"));

            services.AddIdentity<ApplicationUser, IdentityRole>(config =>
                {
                    config.SignIn.RequireConfirmedEmail = true;
                })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.Configure<IdentityOptions>(options =>
            {
                // Omitted
            });

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
                options.SlidingExpiration = true;
            });

           
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "Techsportise API", Version = "v1" });
                c.OperationFilter<AddRequiredHeaderParameter>();
                var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "TechsportiseOnline.xml");
                c.IncludeXmlComments(filePath);
            });

            services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings"));

            services.AddAuthentication()
                .AddCookie()
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.IncludeErrorDetails = true;

                    var secretKey = Configuration.GetSection("JWTSettings:SecretKey").Value;
                    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

                    options.TokenValidationParameters = new TokenValidationParameters
                    {

                        ValidateIssuer = true,
                        ValidIssuer = Configuration.GetSection("JWTSettings:Issuer").Value,
                        ValidateAudience = true,
                        ValidAudience = Configuration.GetSection("JWTSettings:Audience").Value,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = signingKey,

                    };
                    
                });


            services.AddAuthorization();
           

            services.AddMvcCore(option => option.EnableEndpointRouting = false)
                .AddViewLocalization(
                    LanguageViewLocationExpanderFormat.Suffix,
                    opts => { opts.ResourcesPath = "Resources"; })
                .AddDataAnnotationsLocalization()
                .AddApiExplorer();


            services.AddAntiforgery();

            var skipSSL = Configuration.GetValue<bool>("LocalTest:skipSSL");

            // requires using Microsoft.AspNetCore.Mvc;
            services.Configure<MvcOptions>(options =>
            {
                // Set LocalTest:skipSSL to true to skip SSL requrement in 
                // debug mode. This is useful when not using Visual Studio.
                if (!skipSSL)
                {
                    options.Filters.Add(new RequireHttpsAttribute());
                }
            });

            services.AddSingleton<SharedViewLocalizer>();
            services.AddSingleton<SharedViewHtmlLocalizer>();

            services.AddControllersWithViews();

            
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            if (env.EnvironmentName == "Development")
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Techsportise API V1");
            });


            app.UseDefaultFiles();
            app.UseStaticFiles();

          
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
Matthew Warr
  • 86
  • 1
  • 10
  • 31
  • _"these have stopped working"_ how so? Are you getting an error? 401? 403? – abdusco Aug 16 '21 at 09:44
  • Also why are you not using endpoints routing `app.UseEndpoints()`? – abdusco Aug 16 '21 at 09:44
  • The API endpoints return a 401 (I'll update the question) but the MVC endpoints (using the identity cookie) are OK I've not added `app.UseEndpoints()` as it didn;t crop up in the migration guides I was following. I'll add that to see if it has an effect – Matthew Warr Aug 16 '21 at 09:59
  • app.UseEndpoints didn't solve anything – Matthew Warr Aug 16 '21 at 10:21
  • Try to see what is going in with `options.Events.OnAuthenticationFailed`, `AuthenticationFailedContext` should give you some ideas where to look. – Rob Aug 16 '21 at 11:00
  • How/where do I implement that @Rob ? – Matthew Warr Aug 16 '21 at 11:09
  • You will find example on how to do this [here](https://stackoverflow.com/questions/48649717/addjwtbearer-onauthenticationfailed-return-custom-error/50451116#50451116). – Rob Aug 16 '21 at 11:12
  • Thanks for that Rob - I have implemented it. I can see it logging various pieces of information. No 500 error, but it led me to some weirdness. Using swagger, I am getting this back: "Requires an authenticated user" but if I use Postman to the same endpoint, it authenticates... – Matthew Warr Aug 16 '21 at 11:43
  • So it appears as if at least 1 of the issues was a custom filter in swagger which seems to not work. Removing it and using . I am now getting the right behaviour. However in all likelihood the API endpoint was probably working all the time, it was the Swagger UI configuration at fault – Matthew Warr Aug 16 '21 at 11:56

1 Answers1

0

It might be better if you understand the cause of the error, so i'll explain a bit here.

We all use the app.UseAuthentication(); in out startup class, which behind the scenes process like this.

In your above configuration, you provide the middleware 2 schemes to process

services.AddAuthentication()
                .AddCookie()
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.IncludeErrorDetails = true;

                    var secretKey = Configuration.GetSection("JWTSettings:SecretKey").Value;
                    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

                    options.TokenValidationParameters = new TokenValidationParameters
                    {

                        ValidateIssuer = true,
                        ValidIssuer = Configuration.GetSection("JWTSettings:Issuer").Value,
                        ValidateAudience = true,
                        ValidAudience = Configuration.GetSection("JWTSettings:Audience").Value,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = signingKey,

                    };
                    
                });

The first was CookieAuthenticationDefaults.AuthenticationScheme and the second one is JwtBearerDefaults.AuthenticationScheme. Then they both got execute(I know you point out the scheme, but you don't set the default scheme, so Schemes.GetDefaultAuthenticateSchemeAsync() will return something probably not what we want).

Solution: Use a default scheme, and put some logic to forward the pipeline the the right processor!

services.AddAuthentication(opts =>
                {
                    opts.DefaultScheme = "multi-scheme-election";
                    opts.DefaultChallengeScheme = "multi-scheme-election";
                })
                .AddCookie()
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.IncludeErrorDetails = true;

                    var secretKey = configuration.GetSection("JWTSettings:SecretKey").Value;
                    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

                    options.TokenValidationParameters = new TokenValidationParameters
                    {

                        ValidateIssuer = true,
                        ValidIssuer = configuration.GetSection("JWTSettings:Issuer").Value,
                        ValidateAudience = true,
                        ValidAudience = configuration.GetSection("JWTSettings:Audience").Value,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = signingKey
                    };
                })
                .AddPolicyScheme("multi-scheme-election", "Your Election scheme processor here",
                    cfgOpts => cfgOpts.ForwardDefaultSelector = ctx =>
                        ctx.Request.Headers.ContainsKey("Authorization")
                            ? JwtBearerDefaults.AuthenticationScheme
                            : CookieAuthenticationDefaults.AuthenticationScheme);

Done, give it a shot

[HttpGet("TestAuthentication")]
[Authorize] // No need to specify the scheme here, the default policy will take us the right one
public IActionResult TestAuthentication()
{
    return Ok("Authenticated!");
}

P/s: I currently using this too.

Gordon Khanh Ng.
  • 1,352
  • 5
  • 12