WebApi Authorize attribute with services.AddIdenti

2019-08-21 08:26发布

问题:

I have a simple WebApi project, which uses IdentityServer4.AccessTokenValidation to validate tokens issued by an IdentityServer4 Server at development address: https://localhost:44347

I get the token by sending following data to identityserver:

POST
https://localhost:44347/connect/token
client_id:x.api.client
client_secret:secret
response_type:code id_token
scope:X.api
grant_type:client_credentials

Response is:

{
    "access_token": "THETOKEN",
    "expires_in": 1209600,
    "token_type": "Bearer"
}

and sending the token to WebAPi

POST
http://localhost:59062/identity
Authorization:Bearer THETOKEN

I get desired result, but, adding commented part of following code results 404 Not Found.

code is:

public class Startup {

    private const string API_NAME = "X.api";

    public Startup(IConfiguration configuration) {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }


    public void ConfigureServices(IServiceCollection services) {

        string connectionString = Configuration.GetConnectionString("DefaultConnection");

        services.AddLogging(configure => configure.AddConsole());

        services.AddDbContext<MyDataContext>(options => options.UseSqlServer(connectionString));

        services.AddMvcCore()
            .AddAuthorization()
            .AddJsonFormatters()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddTransient<IUserStore<MyUser>, MyUserStore>();
        services.AddTransient<IRoleStore<MyRole>, RoleStore>();
        services.AddTransient<IPasswordHasher<MyUser>, MyHasher>();


        services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options => {
            options.Authority = "https://localhost:44347";
            options.RequireHttpsMetadata = false;
            options.ApiName = API_NAME;
        });


        ////This commented part brokes API
        //services.AddIdentity<MyUser, MyRole>(options => {
        //  options.Password.RequireDigit = true;
        //  options.Password.RequiredLength = 6;
        //  options.Password.RequireNonAlphanumeric = false;
        //  options.Password.RequireUppercase = false;
        //  options.Password.RequireLowercase = false;
        //  options.SignIn.RequireConfirmedEmail = false;
        //})
        //Bekaz we are not using IdentityUser as base
        //.AddUserStore<MyUserStore>()
        //.AddRoleStore<RoleStore>()
        //.AddDefaultTokenProviders();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        if (env.IsDevelopment()) {
            app.UseDeveloperExceptionPage();
        }

        app.UseAuthentication();
        app.UseMvc();
    }
}

the API is as simple as following piece of code(one of identity server's samples)

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace Api.Controllers {
  [Route("[controller]")]
  [Authorize]
  public class IdentityController : ControllerBase {
    [HttpGet]
    public IActionResult Get() {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
  }
}

I use custom User class inherited from IIdentity, custom Role and UserRole, custom RoleStore implemented IRoleStore<MyRole>, and custom UserStore implemented IUserStore<MyUser>, IUserPasswordStore<MyUser>.

EDIT, More Info

this is what i get on console:

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/identity
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Route matched with {action = "Get", controller = "Identity"}. Executing action Api.Controllers.IdentityController.Get ()
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
      Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
      Executing ChallengeResult with authentication schemes ().
[16:48:20 Information] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler
AuthenticationScheme: Identity.Application was challenged.

info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
      AuthenticationScheme: Identity.Application was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action Api.Controllers.IdentityController.Get () in 30.1049ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 103.2969ms 302
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/Account/Login?ReturnUrl=%2Fidentity
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 0.468ms 404

Temporary SOLUTION

there is something with authorization system, I finally changed attribute to what i founded here

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

and it works. but how and why? I don't now yet.

Also, changing the AddAuthentication part to bellow, as mentioned answer suggets, Does Not works and requires the (AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme) to be passed to [Authorize]

        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme    = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddIdentityServerAuthentication(options => {
            options.Authority = "https://localhost:44347";
            options.RequireHttpsMetadata = false;

            options.ApiName = API_NAME;
        });

changing order, finally works.(first AddIdentity and then AddAuthentication)

        services.AddIdentity<MyUser, MyRole>(options => {
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 6;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = false;
            options.Password.RequireLowercase = false;
            options.SignIn.RequireConfirmedEmail = false;
        })
        .AddUserStore<MyUserStore>()
        .AddRoleStore<RoleStore>();


        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddIdentityServerAuthentication(options => {
            options.Authority = "https://localhost:44347";
            options.RequireHttpsMetadata = false;

            options.ApiName = API_NAME;
        });

回答1:

Let me try at explain this, so some other poor soul have some easier time understanding :)

When Authentications are added like above

  services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        ....

It means that every attribute [Authorize] that is put on top of a method or a controller class, will try to authenticate against the default authentication schema (in this case the JwtBearer) AND IT WILL NOT CASCADE DOWN to try to authenticate with other schemas that might be declared (like Cookie schema). In order to make the AuthorizeAttribute authenticate against the cookie schema it has to be specified like in code above

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]

This will work also the other way around, i.e. if cookie schema is default then the JwtBearer schema must be declared for authorization for those methods or controllers that would need JwtBearer token authentication

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]