Monday, 2 December 2013

Claims based authorization in MVC4

Recently I worked on a sample MVC4 application that was using Claims based authentication. I used the Identity and Access Visual Studio extension to help me configuring Windows Identity Foundation (WIF) in my app. In short, the tool updates your web.config by adding sections system.identityModel and system.identityModel.services to enable WIF. In result, my application is redirecting all unauthenticated users to my Identity Provider, which then generates a security token that is returned back to my app.

Once I had the authentication part done I started working on the authorization. I wanted it to be role-based i.e. very similar to what you use by default in the default MVC model:

[Authorize(Roles = "Administrator")]
public class AdminController : Controller
{
    // Controller code here
}
In theory, if your Identity Provider issues a token containing the Identity Role claim (http://schemas.microsoft.com/ws/2008/06/identity/claims/role) with the value of user's current role the above default authorization code should work. And it actually does! This is because some basic claims from the token are automatically used to populate the user's identity object, including roles. So when your app's authorization code checks user's role it will use values provided in the token (if any were provided).

Membership database issue

The above solution worked fine for me at the beginning. What I was not aware of is the fact that, by default, the Authorize attribute also connects to you Membership database, regardless the token content. By default as membership database MVC uses the local ASPNETDB.mdf file. I realized that when I moved the application to a different server, without moving the mdf file. Suddenly I started getting the following SQL exception when calling the Authorize attribute:
A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)
I guess there is an easy way to configure ASP not to connect to the database if roles are provided in the token. However, I decide to take a different approach to have more control over the code.

Custom authorization attribute

I decided to write a custom Authorization attribute, that would search for user's role directly in claims provided in the token:
public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
    private string claimType;
    private string claimValue;

    public ClaimsAuthorizeAttribute(string type, string value)
    {
        this.claimType = type;
        this.claimValue = value;
    }
  
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var identity = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
        var claim = identity.Claims.FirstOrDefault(c => c.Type == claimType && c.Value == claimValue);
             
        if (claim != null)
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
             base.HandleUnauthorizedRequest(filterContext);
        }
    }
} 
This approach is more flexible as it allows me to use different types of claims for authorization in future, not only role. The usage of the attribute is still very simple:
[ClaimsAuthorize(ClaimTypes.Role, "Administrator")]
public class AdminController : Controller
{
    // Controller code here
}

Additional notes

When using claims based authorization it is often advised to use the existing ClaimsPrincipalPermission attribute together with the configured ClaimsAuthorizationManager. In my case this seemed like an overkill, especially that I wanted to keep the code similar to the default authorization model.

1 comment:

Marcos Paulo Honorato said...

Why when we remove the roles authorize does not work automatically, because I need to log out and then a login for work? The problem is that the roles are stored in the cookie. I have to find some solution to update the cookie. When do I remove a roles directly in the database the cookie is outdated. I think I have update the cookie to each User request. The example I'm using is on github https://github.com/aspnet/Musi...