asp.net-core-webapiapi-authorization

Why RequiredScope attribute doesn't have any effect?


According to this Microsoft document you should be able to apply attribute like [RequiredScope("SomeScopeName")] to either controller level or action level to protect the API. But when I try it in my API, it doesn't seem to have any effect at all - regardless what scope name I use (I made sure I don't have the scope by that name in the token), I always get right in to the API actions that I supposed to fail. But at the same time, my policy attributes, such as [Authorize(Policy = "PolicyName")], works just fine. What am I missing?

[ApiController]
[RequiredScope("AnyRandomName")]
public class MyApiController : ControllerBase
{

UPDATE

Here is my Startup.cs

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

    public void ConfigureServices(IServiceCollection services)
    {
        IdentityModelEventSource.ShowPII = true; 
        services.AddControllers();

        services.AddSwaggerGen(opt =>
        {
            opt.CustomSchemaIds(type => type.ToString() + type.GetHashCode()); 
        });

        services.Configure<HostOptions>(Configuration.GetSection(HostOptions.HOST));

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 
        JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
        services.AddAuthentication("Bearer").AddJwtBearer(options =>
        {
            options.Authority = Configuration[HostOptions.IDENTITYGATEWAY];
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false
            };
        });

        services.AddTransient<gRPCServiceHelper>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GroupDemographicEFCore v1"));
        }
        else
        {
            app.UseExceptionHandler("/error");
        }

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

and here is my API controller

[ApiController]
[Authorize]
[RequiredScope("NoSuchScope")]
public class MyApiController : ControllerBase
{
    public MyApiController([NotNull] IConfiguration configuration, [NotNull] ILogger<MyApiController> logger,
        [NotNull] gRPCServiceHelper helper) : base(configuration, logger, helper)
    {
    }

    [HttpGet]
    [Route("/clients/summary")]
    public async Task<IActionResult> ClientsSummaryGet()
    {
        ...

Note that I applied the attributes here on the controller level. But it makes no difference if I move them down to action level - the RequiredScope attributes always gets ignored.

UPDATE-1

I left out the AddAuthorization from my last post update, as I believe it is irrelevant to my issue here. I added it back now, with a few of the policies that I use. Once again, these policies are all working fine, and I don't see how this is relevant to the issue I have.

services.AddAuthorization(options =>
{
    options.AddPolicy("OperatorCode", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("OperatorCode");
    });
    options.AddPolicy("OperatorCode:oprtr0", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("OperatorCode", "oprtr0");
    });
    options.AddPolicy("Role:User+OperatorCode:oprtr0", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("User");
        policy.RequireClaim("OperatorCode", "oprtr0");
    });
    options.AddPolicy("Role:Admin||Role:User", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("Admin", "User");
    });
});

Here is the access_token header

enter image description here

Here is the body of access_token enter image description here enter image description here


Solution

  • What you need to do is to add and configure authorization in Startup.cs like, like this:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthorization(options =>
        {
    
            options.AddPolicy("ViewReports", policy =>
                              policy.RequireAuthenticatedUser()
                                    .RequireRole("Finance")
                                    .RequireRole("Management")
                              );                  
        });
    

    The policy says that the user must be authenticated and be in both roles. In this example RequireAuthenticatedUser() is optional.

    Then you can use that policy like:

    [Authorize(Policy = "ViewReports")]
    public IActionResult ViewReports()
    {
        return View();
    }
    

    To get the role claim to work, you must define what the name of your role claim is in the token, by doing this:

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
         .AddJwtBearer(options =>
         {
               options.TokenValidationParameters.NameClaimType = "name";
               options.TokenValidationParameters.RoleClaimType = "role";
         });
    

    Otherwise the role might not be found, because OpenIDConnect and Microsoft have different opinion on what the claim should be called.

    In the long run, using polices will gives you better and cleaner code, because if you need to change the scopes in the future, you need to update all controllers classes. With a policy , you change it in one place.

    Also, according to this issue at GitHub, it says:

    RequiredScopes just checks at the scp or http://schemas.microsoft.com/identity/claims/scope claims.

    This means that you might need to do some claims transformation (renaming) to get the RequiredScope to map to the scope claim in your access token.

    To complement this answer, I wrote a blog post that goes into more detail about this topic: Debugging OpenID Connect claim problems in ASP.NET Core