Claims-based authorization example with xml-based access policy configuration

  • Tutorial


Introduction


The topic of authentication and authorization will always be relevant for most web applications. Many .NET developers have already managed to get acquainted with the Windows Identity Foundation (WIF), its approaches and capabilities for implementing the so-called "identity-aware" applications. For those who did not manage to work with WIF, the first acquaintance can be started by studying the next section of MSDN . In this article, I propose a more detailed look at the so-called “claims-based” approach to user authorization by examining how this might look with an example.

Claims-Based Authorization


“Claims-Based” authorization is an approach in which the authorization decision to grant or deny access to a specific user is based on arbitrary logic, which uses a certain set of “claims” related to this user as input. Drawing an analogy with the “Role-Based” approach, a certain administrator in his “claims” set will have only one element with the “Role” type and the value “Administrator”, for example. In more detail, about the advantages and problems that this approach solves, you can read on the same MSDN , I also advise you to watch a lecture by Dominic Bayer .

In general, the above approach encourages developers to separate the business logic of the application from the logic of authorization, and this is really convenient. So how does this look in practice? Actually, let's get down to it.

Formulation of the problem


Suppose that you need to create a certain API service that will be available to several client applications. The functionality of client applications is different, users also. It is possible that other client applications will also appear, with their users and the scheme of interaction with the API, so we need to have a flexible authorization system in order to be able at any stage to configure the API access policy for a particular application / user. The API in our case will be built using ASP.NET Web API 2.0, client applications will be, for example, a Windows Phone application and a Web site.

Consider the applications of their users and the functionality in more detail:

Windows Phone Client
Windows phone
  1. By itself, it can only register new users.
  2. Registered users can:
    • View your profile
    • update your profile;
    • change your password;

Web client
Web site
  1. By itself, does not have access to the API.
  2. Users registered with the mobile client can:
    • View your profile
    • update your profile;
    • change your password;

  3. System administrators can:
    • Everything is the same as the users for your account;
    • all the same as the users for the account of any user;
    • view a list of all registered users;
    • create / delete users;

So, we have an idea of ​​what functionality should be provided through the API, to which clients and with what rules. Well, let's get started!

Implementation


Let's start by defining the interface of the future service API:

    public interface IUsersApiController
    {
        // List all users.
        IEnumerable GetAllUsers();
        // Lookup single user.
        User GetUserById(int id);
        // Create user.
        HttpResponseMessage Post(RegisterModel user);
        // Restore user's password.
        HttpResponseMessage RestorePassword(string email);
        // Update user.
        HttpResponseMessage Put(int id, UpdateUserModel value);
        // Delete user.
        HttpResponseMessage Delete(string email);
    }

The direct implementation of the API will be left out of the brackets of this article, at least for example an option like this will do:

    public class UsersController : ApiController
    {
        //...
        public HttpResponseMessage Post([FromBody]RegisterModel user)
        {
            if (ModelState.IsValid)
            {
                return Request.CreateResponse(HttpStatusCode.OK, "Created!");
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
        //...
    }

The next step is to create an inheritor of the ClaimsAuthorizationManager class and override some of its methods. ClaimsAuthorizationManager- this is precisely the WIF component that allows you to intercept incoming requests in one place and execute arbitrary logic, which, based on the set of "claims" of the current user * decides to grant or deny access.

* - we'll talk about where this set is formed a little later.

Without going far, we can borrow its implementation from MSDN at this link . As you can see from the “Examples” section, the following methods are overridden:

    ///  
    /// Overloads  the base class method to load the custom policies from the config file 
    ///  
    /// XmlNodeList containing the policy information read from the config file
    public override void LoadCustomConfiguration(XmlNodeList nodelist)
    {...}
    ///  
    /// Checks if the principal specified in the authorization context is authorized 
    /// to perform action specified in the authorization context on the specified resource 
    ///  
    /// Authorization context
    /// true if authorized, false otherwise
    public override bool CheckAccess(AuthorizationContext pec)
    {...}

Looking at the implementation and comments on it, you can figure out what is happening and I will not dwell on it. I note only the access policy format from this example:

   ...
   
   ...

The access policy here is a set of “policy” sections, each of which is identified by attributes such as “resource” and “action”. Within each such section are listed "claims" that are necessary to access the resource. In the case of WebApi, “resource” is the name of the controller, “action” is the name of the action method. Moreover, it is possible to construct access rules using logical conditions *.

* - and everything would be great if in the current implementation it was possible to configure more than 2 claim elements inside the and and or blocks.

For now, we use everything as-is, except for the name of the heir, we will change it to XmlBasedAuthorizationManager. If you try to build a project, it turns out that we are missing a class PolicyReader, it can be taken from the full source code of the MSDN example.

After the new implementation is ready, we will configure the WebAPI application to use it as an authorization manager. To do this:

1. Register the configuration sections required for WIF to work:


2. We indicate which implementation should be used as an authorization manager:


Well, we told WIF which implementation to use, but as you noticed, two details remained in the configuration above:
  1. instead of a set of xml-sections "policy" we have empty;
  2. there is an xml element " claimsAuthenticationManager", which I did not mention before.

Consider these points in order.

1. Access policy configuration

Returning to the statement of the problem, and also considering the format of the access policy already considered, we will try to make a configuration for our API. This will result in the following set of rules:


         
          -->
         
          -->

We see that some policy sections are simpler, some more complicated, some repeated. Let's consider in parts, starting with a simple option - an access policy to get a list of users :


Everything is very obvious: those users have access to this resource, the set of “claims” of which contains both “claim” elements.

Now a more difficult option is to obtain information about the user by identifier :


         
          -->

Returning to the requirements, only web application administrators can access this resource, as well as users, provided that each user can only receive data from his account . As you can see, we easily establish the first requirement in the first ..block. But what about users?

Unfortunately, the current implementation, which We valiantly copied, does not allow us to configure this condition now. In addition, as I mentioned above , it also does not allow the use and/orof nested elements inside logical " " blocks. To be extremely honest, this implementation rigidly sets the number of "claim" elements equal to two inside the " and/or" blocks.

As for the condition “each individual user can receive data only on his account ”, I plan to offer my own solution in the next article. I propose for now to come to terms with the fact that all users can view information about each other, as it leaves the compiled configuration. Especially while the implementation of the method GetUserByIdlooks like throw new NotImplementedException().

But for the current configuration to work properly, we will slightly change the implementation of the class PolicyReader:

   ///  
   /// Read the Or Node 
   ///  
   /// XmlDictionaryReader of the policy Xml 
   /// ClaimsPrincipal subject 
   /// A LINQ expression created from the Or node 
   private Expression> ReadOr(XmlDictionaryReader rdr, ParameterExpression subject)
   {
       Expression defaultExpr = Expression.Invoke((Expression>)(() => false));
       while (rdr.Read())
       {
           if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "or")
           {
               defaultExpr = Expression.OrElse(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject));
           }
           else
               break;
       }
       rdr.ReadEndElement();
       Expression> resultExpr 
             = Expression.Lambda>(defaultExpr, subject);
       return resultExpr;
   }
   ///  
   /// Read the And Node 
   ///  
   /// XmlDictionaryReader of the policy Xml 
   /// ClaimsPrincipal subject 
   /// A LINQ expression created from the And node 
   private Expression> ReadAnd(XmlDictionaryReader rdr, ParameterExpression subject)
   {
       Expression defaultExpr = Expression.Invoke((Expression>)(() => true));
       while (rdr.Read())
       {
           if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "and")
           {
               defaultExpr = Expression.AndAlso(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject));
           }
           else
               break;
       }
       rdr.ReadEndElement();
       Expression> resultExpr
             = Expression.Lambda>(defaultExpr, subject);
       return resultExpr;
   }

Well, we have configured the access policy for our API resources, created an authorization manager implementation that can work with our configuration. Now you can go to authentication - the stage that precedes authorization.

2. Authentication and ClaimsAuthenticationManager

Before deciding whether the user has access to the resource, authentication must first be made, and if it is successful, the user’s claims should be filled out.

For authentication, we will use Basic Authentication and, for example, its implementation in Thinktecture.IdentityModel.45 . To do this, in the NuGet console, execute the command:

Install-Package Thinktecture.IdentityModel

The class code is WebApiConfigmodified to be approximately as follows:

   public static class WebApiConfig
   {
       public static void Register(HttpConfiguration config)
       {
           var authentication = CreateAuthenticationConfiguration();
           config.MessageHandlers.Add(new AuthenticationHandler(authentication));
           config.MapHttpAttributeRoutes();
           config.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
           );
           config.EnableSystemDiagnosticsTracing();
           config.Filters.Add(new ClaimsAuthorizeAttribute());
       }
       private static AuthenticationConfiguration CreateAuthenticationConfiguration()
       {
           var authentication = new AuthenticationConfiguration
           {
               ClaimsAuthenticationManager = new AuthenticationManager(),
               RequireSsl = false //only for testing
           };
           #region Basic Authentication
           authentication.AddBasicAuthentication((username, password) =>
               {
                   var webSecurityService = ServiceLocator.Current.GetInstance();
                   return webSecurityService.Login(username, password);
               });
           #endregion
           return authentication;
       }
   }

Here I will only note that I use a certain one to check the credentials that came from the request IWebSecurityService. You can use your own logic here, for example: Now, with every request to any resource, an authentication check will be performed, but we also need to transform the basic set of claims of the current user. Our heir to this class, which we have already registered , deals with this , to be exact :return username == password;

ClaimsAuthenticationManager

   public class AuthenticationManager : ClaimsAuthenticationManager
   {
       public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
       {
           if (!incomingPrincipal.Identity.IsAuthenticated)
           {
               return base.Authenticate(resourceName, incomingPrincipal);
           }
           var claimsService = ServiceLocator.Current.GetInstance();
           var claims = claimsService.GetUserClaims(incomingPrincipal.Identity.Name);
           foreach (var userClaim in claims)
           {
               incomingPrincipal.Identities.First().AddClaim(new Claim(userClaim.Type, userClaim.Value));
           }
           return incomingPrincipal;
       }
   }

As you can see, if the user is authenticated, they receive their set of “claims”, say from the database, using the newly created instance IUsersClaimsService. After the "transformation", the ClaimsPrincipal instance is returned further to the pipeline for later use, for example, by authorization.

Check result


It's time to test the performance of our solution. To do this, we will naturally need users with one or another “claims”. We will not fantasize for a long time about where to get them and we will slightly modify them AuthenticationManagerfor testing purposes. Instead of using, IUsersClaimsServiceinsert the following code:

   public class AuthenticationManager : ClaimsAuthenticationManager
   {
       public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
       {
           ...
           if (incomingPrincipal.Identity.Name.ToLower().Contains("user"))
           {
               incomingPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.Role, "User"));
           }
           return incomingPrincipal;
       }
   }

Well, now all users whose login contains the word "user" will contain the desired "claim".
Run the project and follow the link localhost : [port] / api / users


We enter the cherished username and password, our simple authorization check them for equality, and the authorization manager transforms the set of "claims":


We continue the execution and make sure that a mere mortal cannot view the list of all users:


Now let's recall that at the stage of configuring the access policy, we had to allow all users to view information about each other for a while, and we will use this. Let's try to find out about a user with Id = 100 by clicking on the link ~ / api / users / 100:



And now we observe that a certain implementation that appeared on the sidelines returns information about any user :)

Conclusion


So we got acquainted with some of the features of WIF, sorted out an example of where to start when building a flexible authorization system, and also “encoded” a bit.

Thanks for attention.

Also popular now: