Why does my Authorize Attribute not work?

Sad title, isn’t it? The alternative would have been “The complicated relationship between claim types, ClaimsPrincipal, the JWT security token handler and the Authorize attribute role checks” – but that wasn’t very catchy.

But the reality is, that many people are struggling with getting role-based authorization (e.g. [Authorize(Roles = “foo”)]) to work – especially with external authentication like IdentityServer or other identity providers.

To fully understand the internals I have to start at the beginning…

IPrincipal
When .NET 1.0 shipped, it had a very rudimentary authorization API based on roles. Microsoft created the IPrincipal interface which specified a bool IsInRole(string roleName). They also created a couple of implementations for doing role-based checks against Windows groups (WindowsPrincipal) and custom data stores (GenericPrincipal).

The idea behind putting that authorization primitive into a formal interface was to create higher level functionality for doing role-based authorization. Examples of that are the PrincipalPermissionAttribute, the good old web.config Authorization section…and the [Authorize] attribute.

Moving to Claims
In .NET 4.5 the .NET team did a radical change and injected a new base class into all existing principal implementations – ClaimsPrincipal. While claims were much more powerful than just roles, they needed to maintain backwards compatibility. In other words, what was supposed to happen if someone moved a pre-4.5 application to 4.5 and called IsInRole? Which claim will represent roles?

To make the behaviour configurable they introduced the RoleClaimType (and also NameClaimType) property on ClaimsIdentity. So practically speaking, when you call IsInRole, ClaimsPrincipal check its identities if a claim of whatever type you set on RoleClaimType with the given value is present. As a default value they decided on re-using a WS*/SOAP -era proprietary type they introduced with WIF (as part of the ClaimTypes class): http://schemas.microsoft.com/ws/2008/06/identity/claims/role.

So to summarize, if you call IsInRole, by default the assumption is that your claims representing roles have the type mentioned above – otherwise the role check will not succeed.

When you are staying within the Microsoft world and their guidance, you will probably always use the ClaimTypes class which has a Role member that maps to the above claim type. This will make role checks automagically work.

Fast forward to modern Applications and OpenID Connect
When you are working with external identity providers, the chance is quite low that they will use the Microsoft legacy claim types. They will rather use the more modern standard OpenID Connect claim types.

In that case you need to be aware of the default behaviour of ClaimsPrincipal – and either set the NameClaimType and RoleClaimType to the right values manually – or transform the external claims types to Microsoft’s claim types.

The latter approach is what Microsoft implemented (of course) in their JWT validation library. The JWT handler tries to map all kinds of external claim types to the corresponding values on the ClaimTypes class – e.g. role to http://schemas.microsoft.com/ws/2008/06/identity/claims/role.

I personally don’t like that, because I think that claim types are an explicit contract in your application, and changing them should be part of application logic and claims transformation – and not a “smart” feature of token validation. That’s why you will always see the following line in my code:

JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();

..which turns the mapping off. Newer versions of the handler call it DefaultInboundClaimTypeMap.

Setting the claim types manually
The constructor of ClaimsIdentity allows setting the claim types explicitly:

var id = new ClaimsIdentity(claims, “authenticationType”, “name”, “role”);
var p = new ClaimsPrincipal(id);

Also the token validation parameters object used by the JWT library has that feature. It bubbles up to e.g. the OpenID Connect authentication middleware like this:

var oidcOptions = new OpenIdConnectOptions
{
    AuthenticationScheme = "oidc",
    SignInScheme = "cookies",
 
    Authority = Clients.Constants.BaseAddress,
    ClientId = "mvc.implicit",
    ResponseType = "id_token",
    SaveTokens = true,
 
    TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role",
    }
};

Other JWT related libraries have the same capabilities – just have a look around.

Summary
Role checks are legacy – they only exist in the (Microsoft) claims world because of backwards compatibility with IPrincipal. There’s no need for them anymore – and you shouldn’t do role checks. If you want to check for the existence of specific claims – simply query the claims collection for what you are looking for.

If you need to bring old code that uses role checks forward, either let the JWT handler do some magic for you, or take control over the claim types yourself. You probably know by now what I would do ;)

 

…oh – and just in case you were looking for some practical advice here. The next time your [Authorize] attribute does not behave as expected – bring up the debugger, inspect your ClaimsPrincipal (e.g. Controller.User) and compare the RoleClaimType property with the claim type that holds your roles. If they are different – there’s your answer.

Screenshot 2016-08-21 14.20.28

 

 

This entry was posted in .NET Security, OAuth, OpenID Connect, WebAPI. Bookmark the permalink.

11 Responses to Why does my Authorize Attribute not work?

  1. Great post!
    You got your claims from the id token, right? Is it possible to perform the same check at API level using only the access token? In my implementation here, the access token does not include the role claim, I’m guessing it’s default behavior, correct?

    • Doesn’t matter. It is a general post about claims. The authorize attribute exists both in MVC and Web API (and ASP.NET Core as well). They work all the same with regards to role checks.

      The claims that go into the principal depend on whatever the issuer sends and other factors like your claims transformation logic.

  2. Hi Dominick,
    Enlightening post, thank you! Great work as well with OSS IdentityServer4 – fan of it and I have respect for the work. Thanks!

  3. Ammar Rizvi says:

    Hi Dominick,
    I am trying to use [Authorize(Roles = “Admin”)] after modifying one of your samples but I am unable to make it work. I am using IdentityServer4 RC1 update 1. User.IsInRole(“Admin”) and User.HasClaim(“role”, “Admin”), both return false.
    Comparing with the screenshot you have pasted in this blog, i see that your roles are not there as an array while mine are. Something like:
    {role: [ “Admin”, “Geek”]}

    I am also using:
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); and

    TokenValidationParameters = new TokenValidationParameters()
    {
    NameClaimType = “name”,
    RoleClaimType = “role”
    }

    I thought of using Policy instead of Roles but then HasClaim has to work in order for Policy to work with Require claim.

    Kindly advise.

  4. Yann Crumeyrolle says:

    The NameClaimType shouldn’t be the equivalent of the “sub” claim instead of the “name” claim if we are trying to be as closely as possible of the JWT/OIDC standards ?

  5. Pingback: Identity and the Client | Dr Bits

  6. Uriel Biton says:

    Thanks a lot for the great post, the tip at the end is a true time saver :)

  7. Anton Jan Rutten says:

    If roles shouldn’t be used anymore, what is the recommended way of authorizing users?

Leave a comment