Wednesday 18 May 2011

Implementing Customizable Claims-Based Authorization with Windows Identity Foundation

Windows Identity Foundation (WIF) provides the basis for adding claims-based authentication to your Web services (and also to Web applications). It achieves this by adding the necessary plumbing and configuration to your solutions that enable them to interact with a Security Token Service (STS), following the WS-Federation specification. The Windows Identity Foundation SDK includes utilities and assemblies that developers can employ for integrating an STS into a solution, and it also provides a wizard for Visual Studio 2010 that can automate many of the tasks associated with using an STS, including building a simple STS for testing purposes.

The key rationale behind using an STS is to decouple the authentication mechanism from the Web service that requires users to be authenticated. By following the WS-Federation protocol, a Web service can detect whether a user’s session has been authenticated, and if not it can transparently redirect the user’s request via an STS to perform the necessary authentication processing. How the STS actually authenticates the user is up to the STS and is essentially of little concern to the Web service. When authentication is complete, the STS directs the user’s request back to the Web service, but adds a security token to the request that contains information about the identity of the user. The Web service can then examine this token to determine whether or not to authorize access. Now, although the mechanics of the authentication mechanism are of minimal interest to the Web service, determining the privileges of an authenticated user definitely is an important issue.

If you are using WIF, the information in the security token is passed to the code in the Web service that implements each operation via the static Thread.CurrentPrincipal.Identity property. This property is a Microsoft.IdentityModel.Claims.IClaimsIdentity object that contains a collection called Claims. Each item in this collection is an authenticated claim concerning the identity of the user. You can iterate through this collection to find the claim that you are interested in and verify that it matches a selected value before allowing the operation to continue. For example, if you wish to ensure that only users who reside in a particular country can perform the operation, you can check the Claims collection for the Country claim and verify that the value of this claim is appropriate; if not, you can throw a SecurityException and deny access to the user. The following code shows an example that restricts the user to being located in the United Kingdom (the ListProducts method implements an operation that retrieves product names from a database and returns them as a list):

public List<string> ListProducts()
{
// Authz without using WIF infrastructure
ClaimsIdentity id = Thread.CurrentPrincipal.Identity as ClaimsIdentity;

Claim countryClaim = (from claim in id.Claims
where claim.ClaimType == ClaimTypes.Country
select claim).Single();

if (String.Compare(countryClaim.Value, "United Kingdom") != 0)
{
throw new SecurityException("Access Denied");
}
...
}
Note: If you have previously implemented claims-based authentication and authorization with WCF by using technologies such as Windows CardSpace, you will have queried the claims that identify the user through the ServiceSecurityContext property of the OperationContext. WIF reverts to the more standardized technique of examining the Identity property of the Thread.CurrentPrincipal property.

However, although this approach is reasonably straightforward and easy to understand, it does suffer from some issues. Primarily, the authorization code is too tightly integrated into the operation, so if the authorization requirements change (such as expanding the list of countries that a valid user can lives in, or you need to authorize users based on a different claim such as their email address or date of birth), then you need to modify this method and rebuild the service. To counter these concerns, WIF enables you to decouple authorization from the code that needs to be authorized; you can implement a custom authorization manager and insert it into the WIF pipeline.

To build a custom authorization manager, you extend the Microsoft.IdentityModel.Claims.ClaimsAuthorizationManager class and override the CheckAccess method. This method takes an AuthorizationContext object as a parameter, which contains the authenticated claims and which also describes the resource being accessed. This resource might be a Web page (in the case of an ASP.NET Web application), or an operation (in the case of a Web service). You provide logic in the body of the CheckAccess method that retrieves the authenticated claims that identify the user and matches them against the resource or operation, returning true if the user should be permitted to access the resource or operation, but returning false to deny access. The key benefit of this approach is that you can supply the authorization manager as a separate assembly, and then configure the Web service to load this assembly at runtime and integrate it into the WIF infrastructure. To do this, you specify the assembly and type information in the claimsAuthorizationManager element in the microsoft.identityModel section of the configuration file. The following example assumes that the authorization manager is called ProductsServiceAuthorizationManager in the ProductsServiceAuthorization assembly:

<microsoft.identityModel>
<service>
...
<claimsAuthorizationManager type="ProductsServiceAuthorization.ProductsServiceAuthorizationManager,ProductsServiceAuthorization" />
...
</service>
</microsoft.identityModel>
This task can be performed by an administrator without requiring that the code for the Web service itself is modified. If the authorization requirements change, a developer can simply provide an updated version of the authorization manager assembly.

Another important advantage of this strategy is that the authorization manager is able to support run-time customization. An administrator can provide custom configuration information which gets passed to the authorization manager object via a constructor when it is initialized. There is no defined XML schema for this information, and it is up to the code in the authorization manager to validate and parse this information using whatever technique is most appropriate. The following configuration shows one possible example scheme (strongly influenced by Vittorio Bertocci in his book “Programming Windows Identity Foundation”). In this example, the Web service exposes operations named ListProducts, GetProduct, CurrentStockLevel, and ChangeStockLevel; all operations require the user to be resident in the United Kingdom, but in addition the ListProducts operation is also available to users in the United States.


<microsoft.identityModel>
<service>
...
<claimsAuthorizationManager type="ProductsServiceAuthorization.ProductsServiceAuthorizationManager,ProductsServiceAuthorization">
<policy operation="http://contentmaster.com/IProductsService/ListProducts">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" country="United Kingdom"/>
</policy>
<policy operation="http://contentmaster.com/IProductsService/ListProducts">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" country="United States"/>
</policy>
<policy operation="http://contentmaster.com/IProductsService/GetProduct">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" country="United Kingdom"/>
</policy>
<policy operation="http://contentmaster.com/IProductsService/CurrentStockLevel">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" country="United Kingdom"/>
</policy>
<policy operation="http://contentmaster.com/IProductsService/ChangeStockLevel">
<claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" country="United Kingdom"/>
</policy>
</claimsAuthorizationManager>
...
</service>
</microsoft.identityModel>
The constructor for the ProductsServiceAuthorizationManager class shown below parses the configuration information provided with the claimsAuthoriationManager element, and uses it to populate a Dictionary object listing each operation and the claims (countries) required to access the operation. When a user attempts to invoke an operation, WIF first authenticates the user by using an STS, and then authorizes the request by calling the CheckAccess method. If this method returns true, then WIF allows the operation to run, otherwise it causes a SecurityAccessDeniedException to be thrown and returned to the client:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.IdentityModel.Claims;
using System.Xml;
using System.IO;

namespace ProductsServiceAuthorization
{
public class ProductsServiceAuthorizationManager : ClaimsAuthorizationManager
{
private static Dictionary<string, List<string>> policy =
new Dictionary<string, List<string>>();

// Parse nodes with the following format and populate the policy Dictionary
// with the details specifying the requirements for each operation
//
// <policy operation="OperationName">
// <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/country" country="CountryName" />
// </policy>
public ProductsServiceAuthorizationManager(object policyConfiguration)
{
try
{
XmlNodeList policyData = policyConfiguration as XmlNodeList;

foreach (XmlNode policyItem in policyData)
{
XmlTextReader policyReader =
new XmlTextReader(new StringReader(policyItem.OuterXml));
policyReader.MoveToContent();
string operationName = policyReader.GetAttribute("operation");

policyReader.Read();
string claimType = policyReader.GetAttribute("claimType");

if (claimType.CompareTo(ClaimTypes.Country) == 0)
{
string countryName = policyReader.GetAttribute("country");
List<string> countries;
if (policy.ContainsKey(operationName))
{
countries = policy[operationName];
}
else
{
countries = new List<string>();
policy[operationName] = countries;
}
countries.Add(countryName);
}
}
}
catch (Exception ex)
{
}
}

// Check the claim provided in the AuthorizationContext,
// and verify that it matches the requirements for the
// operation specified in the Resource property of the AuthorizationContext
public override bool CheckAccess(AuthorizationContext context)
{
bool result = false;
try
{
string requestedOperation = context.Action.First().Value;
if (policy.ContainsKey(requestedOperation))
{
IClaimsIdentity id = context.Principal.Identity
as IClaimsIdentity;

Claim countryClaim = (from claim in id.Claims
where claim.ClaimType == ClaimTypes.Country
select claim).Single();

result = (from country in policy[requestedOperation]
where String.Compare(countryClaim.Value, country) == 0
select country).Count() > 0;
}

return result;
}
catch
{
return false;
}
}
}
}
Note: For clarity, this code performs minimal error checking. If you are writing code for a production environment you should adopt a more robust approach.

WIF provides a very powerful framework for implementing claims-based authentication quickly and easily. Implementing claims-based authorization can be equally straightforward, and the WIF infrastructure enables you to decouple the authorization process from the resources and operations that require it.

No comments: