6

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

Alexu
  • 1,015
  • 2
  • 12
  • 32

3 Answers3

9

All we need to do is add

services.AddRequiredScopeAuthorization();

For the RequireScopeAttrubute to work, which is what AddMicrosoftIdentityWebApiAuthentication does under the hood to get it to work anyway.

MrEs
  • 675
  • 7
  • 10
2

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

Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • Thanks Tore, but are you saying that this RequireScope attribute is actually part of the Policy-based authorization and therefore needs to be treated like [Authorize(Policy = "MyPolicy")] attribute? Even if so, shouldn't the access fail if I don't have a handler defined for it in an optionos.AddPolicy? I am a little confused here. – Alexu Jan 07 '22 at 17:59
  • I mean you need to add AddAuthorization()..... as well to your code, in my code its just an example from one of my training classes in the subject. I updated my answer – Tore Nestenius Jan 07 '22 at 18:18
  • I do not have problem with [Authorize(Policy = "...")] attributes, as those are standard policy-based authorization. And yes, I use services.AddAuthorization to add handlers for them and they all works great. I just didn't include them in my post as they are irrelevant here. My only issue is that the [RequiredScope("...")] attribute always gets ignored. – Alexu Jan 07 '22 at 18:28
  • I didn't see that in your code, so that's why I added that in my answer. – Tore Nestenius Jan 07 '22 at 18:43
  • Is the Scope claim actually part of the User object? (ClaimsPrincipal)? – Tore Nestenius Jan 07 '22 at 18:44
  • If the scope is not in the list of UserClaims, then you need to add Claims mapping using (Inside JwtBearer...) options.ClaimActions.MapUniqueJsonKey("scope", "scope"); – Tore Nestenius Jan 07 '22 at 18:45
  • Thanks Tore. But I don't think that's my issue. My issue is that, when I DON'T have the scope in my User object, my access to the controller/action should fail but I don't. The [RequiredScope("...")] attribute always seems to get ignored. That contradicts with what described in the document I mentioned in the beginning of my post. – Alexu Jan 07 '22 at 19:48
  • Can you update your question with how you configure AddAuthorization()....? Also I would add [Authorize] before [RequiredScope], does that not help? – Tore Nestenius Jan 08 '22 at 09:46
  • Perhaps also add a sample access token. – Tore Nestenius Jan 08 '22 at 09:52
  • Thanks Tore! As you asked, I have updated with AddAuthorization() and access_token. And no, adding [Authorize] before [RequiredScope] at either controller or action level doesn't change the behavior. – Alexu Jan 10 '22 at 15:48
  • see my updated answer about the role claim – Tore Nestenius Jan 10 '22 at 17:47
  • Thanks again @Tore, but I am not using any role claim for this, all I need is an API scope - if the API scope does not resent in the token, the access should be denied according to the [RequiredScope] attribute. – Alexu Jan 10 '22 at 18:02
  • you used policy.RequireRole("User"); in the question? – Tore Nestenius Jan 10 '22 at 18:13
  • Do you actually see the scope claim inside the User object? – Tore Nestenius Jan 10 '22 at 18:14
  • As you can see from my access_token above, I have two API scopes in it and those are the scopes my [RequiredScope("ScopeName")] is looking for. So that's all fine. The problem is, when I take the scope out of my token, my access to the API should be denied but it is not. – Alexu Jan 10 '22 at 19:36
  • Please do not worry about the policy.RequireRole("User");. I use it for my testing only and it is not relevant here with the issue. That's why I left out those policies in my original post in the first place. – Alexu Jan 10 '22 at 19:38
  • I have a feeling that this issue may not have anything to do with IdentityServer4 configuration. Instead, it is about ASP.Net Core authorization functions in general, and Microsoft.Identity.Web.Resource functions in particular. – Alexu Jan 10 '22 at 19:49
  • 1
    Why don't you use a policy here instead of [RequiredScope("AnyRandomName")]? if the policies works, then use policies everywhere? Polices is better to use than hardcoding requiredscope... – Tore Nestenius Jan 11 '22 at 07:26
  • Yes, that's what I am doing right now. I just want to know why the RequiredScope doesn't work the way it supposed to. Plus, I still feel the RequiredScope is more convenient than policies, as least in some cases, if it works. I have submitted and issue with this to MS. But thanks for all your inputs. Really appreciate it! – Alexu Jan 11 '22 at 16:28
  • Actually, I think polices gives you better code, because if you need to change the scope, you need to update all controllers classes. With a policy , you change it in one place. – Tore Nestenius Jan 11 '22 at 16:38
  • also, this quote from here "RequiredScopes just checks at the scp or http://schemas.microsoft.com/identity/claims/scope claims.", https://github.com/AzureAD/microsoft-identity-web/issues/1571 – Tore Nestenius Jan 11 '22 at 16:41
  • see also https://wrapt.dev/blog/simplifying-user-and-role-based-permissions-in-dotnet?utm_source=csharpdigest&utm_medium=email&utm_campaign=397 – Tore Nestenius Jan 11 '22 at 16:45
  • could you please update your answer with the info about RequiredScope bug, and your recommendation about using policy instead? I will accept it after that. – Alexu Jan 11 '22 at 16:58
  • updated, hope you like it! good? – Tore Nestenius Jan 11 '22 at 17:43
0

My codes:

installing these 2 packages:

<PackageReference Include="Microsoft.Azure.AppConfiguration.AspNetCore" Version="4.5.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.21.1" />

Startup.cs, adding code in ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");
    services.AddControllers();
}

don't forget these two lines in Configure method:

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

My test controller:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;
using System.Collections.Generic;

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    [RequiredScope("User.Read")]
    public class HomeController : ControllerBase
    {
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        [HttpPost]
        public string getRes() {
            return "hello world";
        }
    }
}

Test result :

enter image description here enter image description here

==============================================================

To protect an ASP.NET or ASP.NET Core web API, you must add the [Authorize] attribute to one of the following items:

The controller itself if you want all controller actions to be protected The individual controller action for your API

According to this section's example,

adding [Authorize] before the line [RequiredScope("AnyRandomName")] ?

[ApiController]
[Authorize]
[RequiredScope("AnyRandomName")]
public class MyApiController : ControllerBase
{
Tiny Wang
  • 10,423
  • 1
  • 11
  • 29
  • Thank you @Tiny. But that's not it. I tried what you suggested at both controller level and action level and it didn't make any difference in either case. Any ideas? – Alexu Jan 06 '22 at 15:09
  • Thanks again. As shown in my updates above, I don't see anything jumps out to me as I compare my code with yours, at least from what I can tell. Not sure why my RequireScope attribute keeps getting ignored. The only difference I noticed here is that you are using AzureAd and I use IdentityServer4 as ID provider. But that shouldn't matter, should it? – Alexu Jan 07 '22 at 16:56
  • @Alexu I think azure ad and IdentityServer4 should have some differences and the document you followed is about azure ad, while for identityServer4, you may refer to [this one](https://docs.identityserver.io/en/latest/quickstarts/1_client_credentials.html) – Tiny Wang Jan 10 '22 at 01:26
  • Thanks again @Tiny for the inputs! If I understand correctly, neither IdentityServer4 nor Azure Ad does anything in authorization implementations - they only issue security tokens and, based on those, it is ASP.Net Core implementing the enforcement of the access control. My problem, in my mind, is part of that access control. Am I right? – Alexu Jan 10 '22 at 18:09
  • 1
    I'm not familiar with identityServer4, but on azure ad, it's true that it provides security tokens, but it also provides the access control. The access control contains token decode and check if it's expired, if it has correct role or scope and so on, and these are not written by your code but only add some annotations before your method or class. @Alexu – Tiny Wang Jan 11 '22 at 01:22
  • Well I am not familiar with Azure Ad and maybe you are right about why it's not working. I am now using policies for what I need and so far, it works great. Thanks for your inputs! @Tiny Wang – Alexu Jan 11 '22 at 20:55
  • @Alexu well, that's great, I'm glad to see you worked it out : ) – Tiny Wang Jan 12 '22 at 01:19