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, "[email protected]"), ...
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:
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.