简体   繁体   中英

Core 2 Authorize on API Controller

I have a Web App which I built using .NET Core 2 MVC with Individual User Accounts. The app works properly and the Authorize on the controllers works great, displaying only permitted view etc.

It's all standard, built with guidance from the various tutorials online. It's not doing anything too clever, basic forms doing CRUD operations.

I want to add some REST endpoints that exchange JSON to this application and authorize the endpoints using a JWT as an Authorization (Bearer) header. According to all the tutorials this should be reasonably straightforward as they have been combined but I can't seem to get anything to work.

What seems to happen is, the MVC Authorize overrides the JWTBearer authorization, so I can only access the API actions (Which I want to route as /api/{action}) when I have a logged in cookie.

  1. I need the MVC stuff to be left alone with authorization. That works well.
  2. I want to add API endpoints at /api/{controller}/{action}/{id} I don't mind if this is in the same controller or a different controller
  3. The authorization on /api should be through a JWT bearer token and the MVC stuff through the login cookie as standard. However, both map to the same user (ownerID)

Can anyone point me in the right direction? I have been trying to implement a simple GET method for 3 days and I keep hitting brick walls.

EDIT: Further testing has revealed something interesting. I have installed Swagger to test the requests.

I have added a second controller to handle my API methods. this is on api/Races. This controller has the JWTBearerDefaults as the authentication scheme.

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
  • If I am not logged in through the MVC app, and make a request without a bearer token, it redirects me to login.
  • If I am not logged in through the MVC app, and make the request WITH a (valid) token, it redirects me to login.
  • When I login through MVC and execute my request without a Bearer token I receive a 401 Unauthorized (expected)
  • When I am (still) logged in and execute my request with a valid bearer token I receive a valid response.
  • When I am still logged in an execute my request with an invalid bearer token I get a 401 unauthorized (expected)

So it seems like it is using the Token authentication as a second layer of authorization. What I'd like it to do is, on the /api controllers, use it as the ONLY method of Authorization.

Here is the code from my startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TechsportiseOnline.Data;
using TechsportiseOnline.Models;
using TechsportiseOnline.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using TechsportiseOnline.Authorization;
using TechsportiseOnline.Helpers;
using Swashbuckle.AspNetCore.Swagger;
using System.IO;
using Microsoft.Extensions.PlatformAbstractions;
using static TechsportiseOnline.Helpers.Swagger;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace TechsportiseOnline
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("TechsportiseDB")));
                                                        //options.UseInMemoryDatabase("Teschsportise"));

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

            services.Configure<IdentityOptions>(options =>
            {
                // Password settings
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
                options.Password.RequiredUniqueChars = 2;

                // Lockout settings
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                options.Lockout.MaxFailedAccessAttempts = 10;
                options.Lockout.AllowedForNewUsers = true;

                // User settings
                options.User.RequireUniqueEmail = true;
            });

            services.Configure<AuthMessageSenderOptions>(Configuration);

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.Cookie.Expiration = TimeSpan.FromDays(150);
                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;
            });

            // Add application services.
            services.AddTransient<IEmailSender, Email>();
            //services.AddTransient<ICreateContact>();
            //services.AddTransient<IUpdateContact>();

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

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


            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .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.AddMvc();

            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.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                 .RequireAuthenticatedUser()
                                 .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });

            services.AddScoped<IAuthorizationHandler,
                      OwnerRaceAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  AdminRaceAuthorizationHandler>();

            services.AddScoped<IAuthorizationHandler,
                      OwnerRaceEntriesAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  AdminRaceEntriesAuthorizationHandler>();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            // 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");
            });


        }
    }
}

UPDATED startup.cs to reflect changes in Comments.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TechsportiseOnline.Data;
using TechsportiseOnline.Models;
using TechsportiseOnline.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using TechsportiseOnline.Authorization;
using TechsportiseOnline.Helpers;
using Swashbuckle.AspNetCore.Swagger;
using System.IO;
using Microsoft.Extensions.PlatformAbstractions;
using static TechsportiseOnline.Helpers.Swagger;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace TechsportiseOnline
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("TechsportiseDB")));
                                                        //options.UseInMemoryDatabase("Teschsportise"));

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

            services.Configure<IdentityOptions>(options =>
            {
                // Password settings
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
                options.Password.RequiredUniqueChars = 2;

                // Lockout settings
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                options.Lockout.MaxFailedAccessAttempts = 10;
                options.Lockout.AllowedForNewUsers = true;

                // User settings
                options.User.RequireUniqueEmail = true;
            });

            services.Configure<AuthMessageSenderOptions>(Configuration);

            //services.ConfigureApplicationCookie(options =>
            //{
            //    // Cookie settings
            //    options.Cookie.HttpOnly = true;
            //    options.Cookie.Expiration = TimeSpan.FromDays(150);
            //    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;
            //});

            // Add application services.
            services.AddTransient<IEmailSender, Email>();
            //services.AddTransient<ICreateContact>();
            //services.AddTransient<IUpdateContact>();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Techsportise API", Version = "v1" });
                c.OperationFilter<AddRequiredHeaderParameter>();
                var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "Techsportise.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.AddAuthentication()
                .AddCookie() 
                .AddJwtBearer(options =>
                {
                    options.Audience = "xyz";
                    options.Authority = "yzx";
                });

            services.AddMvc();

            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.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                 .RequireAuthenticatedUser()
                                 .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });

            services.AddScoped<IAuthorizationHandler,
                      OwnerRaceAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  AdminRaceAuthorizationHandler>();

            services.AddScoped<IAuthorizationHandler,
                      OwnerRaceEntriesAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  AdminRaceEntriesAuthorizationHandler>();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();


            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            // 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");
            });


        }
    }
}

Added a brand new TestController , copying your code.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TechsportiseOnline.Controllers
{
    public class TestController : Controller
    {
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Based on Scheme it will auth, for cookie mention [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
        [Route("api/[controller]")]
        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";
            return View();
        }
    }
}

You mean, without login making request with valid token should pass? you can achieve this by removing cookie authentication scheme or JWTbearer scheme at ConfigureServices() level.

services.AddAuthentication(   // no Authenticationschemes mentioned here  )
                .AddCookie() //CookieAuthentication
                .AddJwtBearer(options =>
                {
                    options.Audience = "xyz";
                    options.Authority = "yzx";
                });

If you had valid token then with out login you can hit any mvc controllers or web api controllers without redirecting to any login pages. like;

        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Based on Scheme it will auth, for cookie mention [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";
            return View();
        }

I found the problem. When comparing with the blank version of the app I found this AuthorizationBuilder.

    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

I added this whilst I was adding different roles to my app. Taking this away resolves the problem and still seems to be restricting my app to authorized users only.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM