0

In Startup.cs of my API, I have the following authorization policies.

public void ConfigureServices(IServiceCollection services)
{
  services.AddControllers();
  services.AddAuthentication(...);
  ...
  services.AddAuthorization(options =>
  {
      options.AddPolicy("VerySecurePolicy", policy =>
      {
          policy.RequireClaim("admin");
      });
      options.AddPolicy("VaguelySecurePolicy", policy =>
      {
          policy.RequireAuthenticatedUser();
      });
  });
}

Then, I protect two action methods, one with parameterless attribute and one with a policy specified.

[Authorize, HttpGet("regular")]
public IActionResult GetRegularData() { return Ok("This is regular level data."); }

[Authorize(Policy = "VerySecurePolicy"), HttpGet("admin")]
public IActionResult GetAdminData() { return Ok("This is admin level data."); }

After login, I can access the former but not the latter. My deduction is that the claim admin isn't assigned properly on my user and I can't see what I'm missing. Checking the user info endpoint (/connect/userinfo) with my access token gives me the ID, email etc. but not the role admin. Inspecting token itself shows no claims array at all (only scopes and the usual claims like sub, exp etc.).

This is the TestUser instance logged in.

yield return new TestUser
{
    SubjectId = "37cfad39-e4da-486b-a8db-a752565125f8", ...
    Claims = new List<Claim>
    {
        new Claim(JwtClaimTypes.Email, "fakey.uno@touchtech.comm"), ...
        new Claim(JwtClaimTypes.Role, "admin")
    }
};

One of the API scopes declared contains admin as a claim. I've verified that scope to be in the access token. I also added info in an API resource like so (although I'm not sure it's actually needed for this).

yield return new ApiScope("test_scope_a1", "Test scope A1", new[] { "admin" });

yield return new ApiResource
{
    ...
    Scopes = new List<string> { "test_scope_a1", ... },
    UserClaims = new List<string> { "admin", ... }
};

Proof of effort:

Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438
  • Your entries for `HttpGet` are mixed up, so I think perhaps you're testing the wrong one? – ProgrammingLlama Jul 28 '21 at 06:59
  • 1
    @Llama Good eyes, mate. Sadly, that can't be the issue. I'm testing both endpoints and get the results as described. One of those works and the other doesn't. I just corrected in my code for sanity's sake and the misbehavior remains. Again, though - good eyes. – Konrad Viltersten Jul 28 '21 at 07:09
  • `UserClaims = new List { "admin", ... }` -> this should be `"role"` – abdusco Jul 28 '21 at 07:52

1 Answers1

2

Your VerySecurePolicy policy isn't set up correctly. Define the policy like so. Note the claimType parameter is ClaimTypes.Role, not role [1].

options.AddPolicy("VerySecurePolicy", policy => {
    policy.RequireClaim(claimType: ClaimTypes.Role, "admin"); 
});

Or, if that's more appropriate, use the other require methods.

options.AddPolicy("VerySecurePolicy", policy => {
    policy.RequireRole("admin");
});

There are quite a few such methods, and they can be chained for sophisticated security definition.

policy.RequireAuthenticatedUser()
    .RequireClaim("client_id", "spa_client")
    .RequireRole("admin")
    .RequireAssertion(context => context.User.Name == "Konrad");
});

What you actually have in your original sample is a check for admin claim, not a role: admin claim.

Also, you can access the former action, because you didn't specify a policy, and don't have a default/fallback policy. That means [Authorize] only ensures the user is authenticated.

For it to work properly, you either need to specify it explicitly:

[Authorize("VaguelySecurePolicy"), HttpGet("admin")]
public IActionResult GetRegularData() { return Ok("This is regular level data."); }

Or set it as the fallback/default policy:

services.AddAuthorization(
    options => {
        options.AddPolicy("VerySecurePolicy", policy => { policy.RequireClaim("role", "admin"); });
        // 
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    }
);

This ensures that the actions annotated with [Authorize] are subject to the fallback policy if they don't have a specific policy set.


[1]: We're not using role claim type, but rather its mapped equivalent http://schemas.microsoft.com/ws/2008/06/identity/claims/role. If you don't want this mapping to occur, and refer to claims as they appear in JWT, you need to disable the mapping:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(...).AddJwtBearer(...);

Now the role claim will appear as role, sub will appear as sub as you'd expect.

Beware that this breaks a number of things, such as, you can't now use User.IsInRole("rolename") to check for roles, because it expects the role claim type to be ClaimTypes.Role. So I'd just leave it as is.

References

abdusco
  • 9,700
  • 2
  • 27
  • 44
  • First of all, you're perfectly right that I missed the role/claim thing. Secondly, great extra info on fallback policy (not needed now but will later today). Now, I still don't get it to work and, based on your great answer, I suspect there's something more that is wrong. When I check the user on */connect/userinfo*, I don't see the claims from the test user definition. Nor do I see them among the claims of the logged in user's `Identity` field. Not in the access token neither. Should I see them there? Any idea what I'm missing in the definition? Or is the issue likely elsewhere? – Konrad Viltersten Jul 28 '21 at 07:33
  • Also, I'm curious regarding the point of fallback policy in the example you provided. Since, as you mentioned, the attribute `Authorize` (no parameters) only checks if the user is authenticated, having `RequireAuthenticatedUser()` as a fallback doesn't add anything extra (in this particular case). Right? I'm thinking, we need (at least) to have the parametersless authorization attribute to invoke a call to `AddAuthorization` to begin with so getting there is already implying that we have an authenticated user. – Konrad Viltersten Jul 28 '21 at 07:38
  • You don't have to have `[Authorize]` attribute, you can set it globally as Mvc filter or endpoint metadata (`endpoints.MapControllers().RequireAuthorization()`), or use a fallback policy. [Fallback policy authorizes everything, default policy provides a default for the parameterless `[Authorize]` attributes.](https://andrewlock.net/setting-global-authorization-policies-using-the-defaultpolicy-and-the-fallbackpolicy-in-aspnet-core-3/#using-the-fallbackpolicy-to-authorize-everything) – abdusco Jul 28 '21 at 07:46
  • _Should I see them there?_, yes for the auth checks to work, the `User`/`ClaimsPrincipal` has to have those claims. If the access token doesn't include those, you might have missed a scope or you might need to implement an `IProfileService`: https://docs.identityserver.io/en/latest/reference/profileservice.html – abdusco Jul 28 '21 at 07:49
  • 1
    All right. It's clearer now. I guess you have answered the original question. Regrettably, I can't confirm it yet due to something else that's not right in my setup. But I sense that your answer is correct and I'll formulate the next issue in a separate question. I'll comment you the link to it in case you've got something brilliant to chip in with. I have *role:admin* in my access token now. Still, the auth doesn't kick in as supposed to and I notice that the claim isn't listed on */connect/userinfo* endpoint. Might have to do with that. – Konrad Viltersten Jul 28 '21 at 12:16
  • Found it! Your answer is totally correct but I want it to be reformulated because it **will** be confusingly misleading as duck to some developers. It works with *role/admin* just like you said **but only** if there's a single *role* in the claim list! If you have multiple roles, the claim becomes array-valued (i.e. *"role":["monkey","donkey"]*) and then it's not equal to *"admin"* as a value. I'll make an edit to your answer. Feel free to rephrase or reject, if you mind. – Konrad Viltersten Jul 28 '21 at 12:55
  • `RequireClaim` works by accepting a set of accepted values. Having an array should change anything, because the `ClaimsPrincipal` has separate claims for all the role claims in the access token. See: https://github.com/dotnet/aspnetcore/blob/18a926850f7374248e687ee64390e7f10514403f/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs#L52-L72 – abdusco Jul 28 '21 at 13:05
  • Check the output of `User.Claims.Select(c => new { c.Type, c.Value }).ToList()` in a controller, you should have multiple role claims. (assuming the access token has multiple role claims) – abdusco Jul 28 '21 at 13:11
  • @KonradViltersten See this gist that shows why it doesn't matter if there's one or multiple role claims. https://gist.github.com/abdusco/0261a443ff8748bd91351853ec136d19 – abdusco Jul 28 '21 at 19:55
  • You are **right** and I stand corrected. The observations I made, led me to draw certain conclusions that were wrong, due to other factors kicking in. My ignorance of the domain prevented me from realizing the underlying patter of how API scopes, API resources, ID resources and audiences cooperate. It's confusing as duck and I've had the aha!-moment now three times. Hopefully, this is the final aha... The only thing worse than the sensation of not understanding a topic is the sensation of understanding, while not actually understanding it! – Konrad Viltersten Jul 30 '21 at 04:36
  • You've been patient and thorough. I want you to know that I do appreciate your effort, especially that it led to (my third) aha-moment, hehe. I can smack you a bounty if you feel that you'd enjoy it. (I believe you deserve it but some people don't care and there's no point throwing perfectly good rep away. But if you appreciate it the slightest, just let me know and I'll be more than delighted to suck it over to you.) – Konrad Viltersten Jul 30 '21 at 04:39
  • Auth domain is complicated and tricky, and you really go through a lot of "aha" moments until it finally clicks :). I've been through some and still have ways to go. What helped me immensely is reading & debugging the source code for ASP.NET Core auth & Identity Server. There's an overwhelming amount of domain knowledge hidden in there. Totally agree on false "aha"s, hahah. There are other ways to gift reps to someone, you know, you can upvote a couple of answers you deem worthy. Same effect, without losing reps yourself. Not saying you should, I don't mind it in the slightest. – abdusco Jul 30 '21 at 06:46