Service Station
Authorization In WCF-Based Services
Dominick Baier and Christian Weyer
Code download available at:MSDN Code Gallery(175 KB)
Contents
Role-Based Authorization
ASP.NET Role Provider
Custom Principal
Centralizing Authorization Logic
Claims-Based Authorization
Claim
Claim Sets
AuthorizationContext
Claims Transformation
Security Token Services
Once you start adopting service-oriented principles for your distributed applications, the security challenges become slightly different. You suddenly realize that you are crossing a security boundary for every service call you make.
Typically—though not necessarily—there will be a network between the sender and the recipient of a service call. While authentication is usually handled automatically by the communication framework, you still have to come up with your own authorization strategy and infrastructure.
Windows Communication Foundation (WCF) provides powerful facilities for implementing authorization in services. You have the choice between an easy-to-use, role-based system as well as a more powerful, but more complex, claims-based API. The remainder of this article will compare both systems and show how to use them to implement robust service authorization.
Role-Based Authorization
The idea behind role-based authorization is that you associate a list of roles with a user. At run time, the service code queries the list to make security-related decisions. These roles can come from the Windows security system (where they are called groups) or from some custom store, such as a database.
The roles API in the Microsoft .NET Framework is based on two interfaces called IIdentity (identity information) and IPrincipal (role information). An IIdentity-derived class holds information such as the name of the user, whether he is authenticated, and how he was authenticated.
An IPrincipal-derived class must implement a single method called IsInRole where you can pass in a role name and get a Boolean response. In addition, the principal has a reference to the identity class it wraps.
The .NET Framework ships with several implementations of theses interfaces. WindowsIdentity and WindowsPrincipal wrap details about a Windows user. GenericIdentity and GenericPrincipal can be used to represent a custom, authenticated user.
There is also a location where you can store a principal to associate it with the currently executing thread in your application (for example, a WCF operation request). This is the CurrentPrincipal thread static property on the System.Threading.Thread class. The typical course of events is that one part of an application populates Thread.CurrentPrincipal so that another part of the application can reach into this property to grab the IPrincipal implementation stored there. Then the IsInRole method can be called on that class to query the role list of the current user to ensure that the caller is authorized for the operation.
This is exactly how it works in WCF. Based on the configured authentication and credential type, WCF creates a corresponding IIdentity implementation—a WindowsIdentity for Windows authentication, a GenericIdentity for most other cases. Then, based on the configuration of the WCF service behavior, ServiceAuthorization, an IPrincipal implementation is created that wraps this identity and provides role information. This IPrincipal is then set on Thread.CurrentPrincipal so that it is accessible to the service operations.
Aside from programmatically calling IsInRole to perform role-based security checks, there is also an attribute called PrincipalPermissionAttribute that allows you to annotate your service operations with role requirements. PrincipalPermissionAttribute will signal a negative authorization outcome to your client by throwing a SecurityException prior to executing the service operation. WCF will catch this exception and turn it into an Access Denied fault message that is returned. The WCF client plumbing will turn this special fault into a .NET exception of type SecurityAccessDeniedException. Figure 1 shows the various ways to do a role check and perform error handling.
Figure 1 Role Check with Error Handling
Service
class Service : IService {
// only 'users' role member can call this method
[PrincipalPermission(SecurityAction.Demand, Role = 'users')]
public string[] GetRoles(string username) {
// only administrators can retrieve the role information for other users
if (ServiceSecurityContext.Current.PrimaryIdentity.Name != username) {
if (Thread.CurrentPrincipal.IsInRole('administrators')) {
...
}
else {
// access denied
throw new SecurityException();
}
}
}
}
Client
var factory = new ChannelFactory<IService>('*');
factory.Credentials.UserName.UserName = 'bob';
factory.Credentials.UserName.Password = 'bob';
var proxy = factory.CreateChannel();
try {
Console.WriteLine('\nBob: roles for Bob --');
proxy.GetRoles('bob').ToList().ForEach(i => Console.WriteLine(i));
Console.WriteLine('\nBob: roles for Alice --');
proxy.GetRoles('alice').ToList().ForEach(i => Console.WriteLine(i));
}
catch (SecurityAccessDeniedException) {
Console.WriteLine('Access Denied\n');
}
The ServiceAuthorization service behavior controls the creation of the IPrincipal instance to be associated with the request thread. By default, WCF assumes Windows authentication and tries to populate Thread.CurrentPrincipal with a WindowsPrincipal.
When your clients are not Windows users, you have the choice between getting roles from an ASP.NET role provider or implementing a custom authorization policy in order to get the roles from a custom database. Another alternative is to implement a custom IPrincipal type and then take complete control over the IsInRole implementation.
ASP.NET Role Provider
WCF can use an ASP.NET role provider to retrieve roles for a user. You can either use one of the built-in providers (for SQL Server or Microsoft Authorization Manager) or write one of your own by deriving from System.Web.Security.RoleProvider.
To make the connection to the role provider, WCF attaches a RoleProviderPrincipal to the executing thread that forwards all calls to IsInRole to the role provider's IsUserInRole method. This method takes the user name and role in question and returns true if the user is a member of that role and false if not.
Figure 2 shows a sample custom role provider along with the necessary configuration entries. WCF will call the role provider for every role check that occurs in your service. If you have a lot of role checks, it would be beneficial to implement some sort of caching strategy to avoid excessive round-trips to the role store.
Figure 2 Sample Role Provider and Configuration Entries
Role Provider
class CustomRoleProvider : RoleProvider {
public override string[] GetRolesForUser(string username) {
if (username == 'administrator') {
return new string[] { 'administrators', 'users' };
}
else {
return new string[] { 'sales', 'marketing', 'users' };
}
}
public override bool IsUserInRole(string username, string roleName) {
return GetRolesForUser(username).Contains(roleName);
}
...
}
Configuration
<behaviors>
<serviceBehaviors>
<behavior name='security'>
<serviceAuthorization
principalPermissionMode='UseAspNetRoles'
roleProviderName='CustomProvider'
</serviceAuthorization>
</behavior>
</serviceBehaviors>
</behaviors>
<system.web>
<roleManager
enabled='true'
defaultProvider='CustomProvider'>
<providers>
<add name='CustomProvider'
type='Service.CustomRoleProvider, Service' />
</providers>
</roleManager>
</system.web>
Customizations like caching could be implemented in the role provider itself or via a custom IPrincipal implementation. That's the next option we will explore.
Custom Principal
Another option is to supply your custom IPrincipal implementation to WCF. This gives you the chance to implicitly run code after the authentication stage of each request. For this you have to create your own custom principal and return it to the WCF plumbing. The custom principal will then be available from Thread.CurrentPrincipal to the service code. Custom principals allow full customization of role-based security and expose specialized security logic for the service developer to use.
Writing a custom principal is straightforward. Simply implement the IPrincipal interface and add your own custom functionality. To integrate the principal with WCF, you have to set the PrincipalPermissionMode attribute in the ServiceAuthorization element to "custom" and provide an authorization policy that is responsible for creating the principal and giving it back to WCF.
An authorization policy is simply a class that implements the System.IdentityModel.Policy.IAuthorizationPolicy interface. Implementations of this interface must employ a method called Evaluate, which gets called on every request. Here you can reach into the WCF service security context and set the custom principal. Figure 3 shows a custom principal as well as the authorization policy and its corresponding configuration entries.
Figure 3 A Custom Principal and Authorization Policy
Principal
class CustomPrincipal : IPrincipal {
IIdentity _identity;
string[] _roles;
Cache _cache = HttpRuntime.Cache;
public CustomPrincipal(IIdentity identity) {
_identity = identity;
}
// helper method for easy access (without casting)
public static CustomPrincipal Current {
get {
return Thread.CurrentPrincipal as CustomPrincipal;
}
}
public IIdentity Identity {
get { return _identity; }
}
// return all roles (custom property)
public string[] Roles {
get {
EnsureRoles();
return _roles;
}
}
// IPrincipal role check
public bool IsInRole(string role) {
EnsureRoles();
return _roles.Contains(role);
}
// cache roles for subsequent requests
protected virtual void EnsureRoles() {
// caching logic omitted – see the sample download
}
}
Authorization Policy
class AuthorizationPolicy : IAuthorizationPolicy {
// called after the authentication stage
public bool Evaluate(EvaluationContext evaluationContext,
ref object state) {
// get the authenticated client identity from the evaluation context
IIdentity client = GetClientIdentity(evaluationContext);
// set the custom principal
evaluationContext.Properties['Principal'] =
new CustomPrincipal(client);
return true;
}
// rest omitted
}
Configuration
<behaviors>
<serviceBehaviors>
<behavior name='security'>
<serviceAuthorization principalPermissionMode='Custom'>
<authorizationPolicies>
<add policyType='Service.AuthorizationPolicy, Service' />
</authorizationPolicies>
</serviceAuthorization>
</behavior>
</serviceBehaviors>
</behaviors>
Centralizing Authorization Logic
So far you have seen how you can access the role information that is provided by the WCF security system from within your service operations. Sometimes, however, it is also useful to have centralized logic that can inspect every incoming request and make authorization decisions without having to spread that logic over all your service operations. Enter the service authorization manager.
The service authorization manager is a class that derives from System.ServiceModel.ServiceAuthorizationManager. You can override the CheckAccessCore method to run custom authorization code for each request. In CheckAccess, you have access to the current security context as well as the incoming message, including the headers. When you return false from CheckAccess, WCF will create the Access Denied fault message and send that back to the client. A return value of true will grant access to the service operation.
You can find the unique identifier of the operation that the client tries to call in the WS-Addressing action header. This value can be obtained from IncomingMessageHeader.Action property on the operation context that is passed into CheckAccess. The format of the action value is:
ServiceNamespace/ContractName/OperationName
For example, you might see something like this:
urn:msdnmag/ServiceContract/GetRoles
WCF passes in the complete request message as a ref parameter. This allows inspecting and even changing the message before it reaches the service operation (or gets bounced back because of a negative authorization outcome).
Be aware that messages in WCF are always read-once. That means you first have to create a copy of the message before you inspect the body. This can be done by creating a message buffer using the following code (you need to be careful with large or streamed messages):
MessageBuffer buffer =
operationContext.RequestContext.RequestMessage.CreateBufferedCopy(
int.MaxValue);
Once you have created a copy of the message, you can use the standard APIs to get access to its content. In Figure 4 , you can see a sample implementation of a service authorization manager that moves the authorization logic from the service operation to a central place.
Figure 4 A Service Authorization Manager
Code
class AuthorizationManager : ServiceAuthorizationManager {
public override bool CheckAccess(
OperationContext operationContext, ref Message message) {
base.CheckAccess(operationContext, ref message);
string action = operationContext.IncomingMessageHeaders.Action;
if (action == 'urn:msdnmag/IService/GetRoles') {
// messags in WCF are always read-once
// we create one copy to work with, and one copy for WCF
MessageBuffer buffer = operationContext.RequestContext.RequestMessage.
CreateBufferedCopy(int.MaxValue);
message = buffer.CreateMessage();
// get the username value using XPath
XPathNavigator nav = buffer.CreateNavigator();
StandardNamespaceManager nsm = new
StandardNamespaceManager(nav.NameTable);
nsm.AddNamespace('msdn', 'urn:msdnmag');
XPathNavigator node = nav.SelectSingleNode
('s:Envelope/s:Body/msdn:GetRoles/msdn:username', nsm);
string parameter = node.InnerXml;
// check authorization
if (operationContext.ServiceSecurityContext.PrimaryIdentity.Name ==
parameter) {
return true;
}
else {
return (GetPrincipal(operationContext).IsInRole(
'administrators'));
}
}
return true;
}
// rest omitted
}
Configuration
<serviceAuthorization
serviceAuthorizationManagerType='Service.AuthorizationManager, Service'>
Claims-Based Authorization
Once you reach a critical mass of services and complexity, role-based security may not be powerful or flexible enough. It doesn't take much for an enterprise scale, service-oriented application to become extremely complicated. Various clients using various client frameworks and credential types talk to various back-end services. These back-end services, in turn, call other services. Sometimes this is done in a trusted subsystem fashion where only the direct caller is authorized. Alternately, sometimes the original caller identity has to be forwarded throughout the call chain. The situation starts to get really complex as soon as external clients (such as partners or customers) need access to some of your internal services.
It quickly becomes apparent that, in a more complex system, the capabilities of IIdentity and IPrincipal may not be sufficient to model identity and authorization data. Role-based security is limited to binary decisions and does not allow arbitrary data to be associated with a subject. A technology-neutral format is needed that allows describing the identities of entities participating in your distributed system.
This is why the .NET Framework, starting in version 3.0, contains a new identity and access-control API that can handle these requirements with a claims-based approach. You can find this new API in the System.IdentityModel assembly and namespace. Furthermore, Microsoft just recently released a preview of its new identity framework code-named "Zermatt." This new framework builds upon the classes and concepts found in System.IdentityModel and makes it easier to build claims-based security into services and applications. It is important to note that System.IdentityModel as well as Zermatt are not bound in any way to WCF and can be used to claims enable any .NET application. It just happens that these APIs are deeply integrated into WCF.
The most important structural classes from the System.IdentityModel namespace that you need to understand are called Claim, ClaimSet, AuthorizationContext, and AuthorizationPolicy. We will now have a closer look at each of them and explore how they are integrated into the WCF security system.
A claim is a piece of information that can be associated with an entity in your system. This is most commonly a user but could also be a service or some resource. A claim consists of three pieces of information: a claim type, the claim content, and whether the claim describes the identity of the subject or a capability of the subject. This data structure is represented by a class known as System.IdentityModel.Claims.Claim. Furthermore, Claim is a DataContract, which makes it serialization-friendly (important when you plan to transmit claims over service boundaries):
[DataContract(Namespace =
'https://schemas.xmlsoap.org/ws/2005/05/identity')]
public class Claim {
[DataMember(Name = 'ClaimType')]
public string ClaimType;
[DataMember(Name = 'Resource')]
public object Resource;
[DataMember(Name = 'Right')]
public string Right;
}
ClaimType is a URI that identifies the claim type. While you can come up with your own type URI, several standard claim types are available from the ClaimTypes class. A claim representing a name could use the URI value held by ClaimTypes.Name:
https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
The Resource property contains the actual claim value. Note that the Resource is of type object—this means that you could associate arbitrarily complex information with that claim (from a simple string to a complete graph). Just keep in mind that this may become a problem when it comes to serialization, and if that is a concern, you should probably stick to primitive types. The purpose of the Right property will become clearer once we talk about claim sets.
You can create a new claim by either using the Claim constructor or taking advantage of one of the static methods on the Claim class (for standard claim types):
Claim purchaseLimitClaim = new Claim(
'https://www.leastprivilege.com/claims/purchaselimit',
5000,
Rights.PossessProperty);
Claim nameClaim = Claim.CreateNameClaim('Alice');
A claim usually doesn't arrive by itself. Claims are typically grouped in claim sets. The class ClaimSet holds a list of claims as well as a reference to the issuer of those claims:
[DataContract(Namespace='https://schemas.xmlsoap.org/ws/2005/05/identity')]
public abstract class ClaimSet : IEnumerable<Claim>, IEnumerable {
public abstract ClaimSet Issuer { get; }
public abstract Claim this[int index] { get; }
public static ClaimSet System { get; }
public static ClaimSet Windows { get; }
}
The issuer is an important concept once your distributed system gets more complex. Claims could come from various sources such as the current service, the calling service, or a security token service. Having an issuer associated with the set allows the service to distinguish between claim sources (which may also influence trust decisions).
An issuer is also described as a claim set, and System.IdentityModel ships with two predefined issuer claim sets called System (for claims coming from the system) and Windows (for claims coming from the Windows security subsystem). Both are available as static properties on the ClaimSet class.
Another important concept is the claim set's identity. A claim set typically needs a single claim that uniquely identifies the subject it describes. This is where the Right property on the Claim class comes into play. A claim set should have a single claim that has a Right value of Identity (this is the unique identifier) and a number of claims with a Right value of PossessProperty (additional claims describing the subject).
You can create your own claim sets by creating a DefaultClaimSet instance or, when you need more control, by deriving from the abstract class ClaimSet:
DefaultClaimSet myClaimSet =
new DefaultClaimSet(ClaimSet.System, new List<Claim> {
new Claim(ClaimTypes.Name, 'Alice', Rights.Identity),
new Claim(purchaseLimitClaimType, 5000, Rights.PossessProperty),
Claim.CreateMailAddressClaim(new MailAddress('alice@leastprivilege.com'))
});
System.IdentityModel ships with two claim sets for converting Windows tokens and X.509 certificates to claims—these are WindowsClaimSet and X509CertificateClaimSet, respectively. These two claim sets are used by WCF for the Windows and Certificate client credential types.
ClaimSet provides two processing primitives for querying the claims: FindClaims and ContainsClaim. FindClaims returns a collection of claims for the specified claim type, whereas ContainsClaim gives you information about the existence of a specific claim.
Due to the general-purpose nature of claim sets, you will typically end up writing your own domain-specific extensions that work against the claim set data structure. Extension methods in C# 3.0 are a convenient way to extend these types with custom functionality. (I have written a library of extension methods for Claim, ClaimSet, and related types that you can download from leastprivilege.com/IdentityModel .)
AuthorizationContext
The last missing piece in the IdentityModel puzzle that you need to know before doing some real work with claims is the authorization context, which acts as a container for claim sets as well as transformation policies (more on this later). WCF makes the authorization context available via the thread-static ServiceSecurityContext. You can use the code snippet shown in Figure 5 to dump the claim sets and claims that are associated with the current service operation request.
Figure 5 Finding Claims and Claim Sets
public void ShowAuthorizationContext() {
AuthorizationContext context =
ServiceSecurityContext.Current.AuthorizationContext;
foreach (ClaimSet set in context.ClaimSets) {
Console.WriteLine('\nIssuer:\n');
Console.WriteLine(set.Issuer.GetType().Name);
foreach (Claim claim in set.Issuer) {
ShowClaim(claim);
}
Console.WriteLine('\nIssued:\n');
Console.WriteLine(set.GetType().Name);
foreach (Claim claim in set) {
ShowClaim(claim);
}
}
}
private void ShowClaim(Claim claim) {
Console.WriteLine('{0}\n{1}\n{2}\n',
claim.ClaimType,
claim.Resource,
claim.Right);
}
When you place this code into a WCF service, you will see different output depending on the configured client credential type. If Windows authentication is enabled, the WCF-generated claim set will contain the user's SID (identity claim), the group's SIDs, and the user name. For a request that is authenticated using a client certificate, the claim set will contain claims that describe the subject name, public key, thumbprint (identity claim), expiration dates, and so on. For users that authenticate with a simple user name/password pair, there will be only a single user name identity claim.
So you can see that the WCF integrated claims layer converts technology-specific identity information, such as a Windows token or certificate, to a general-purpose data structure that can be queried using standard APIs. This makes writing services that have to support multiple credential types much easier—you are not hardwired to any technology-specific APIs anymore. If new credential types are added to WCF, the corresponding plumbing will take care of converting that proprietary format to claims as well.
Claims Transformation
Service operations typically don't care about the user's SID or certificate thumbprint, but they do care about domain-specific identity information such as a user identifier, e-mail address, or purchase limit. Claims transformation is the process of transforming the technology-specific identity details into application-specific ones.
Now that you have all identity information in a common format, it is easy to parse and analyze the claims information to create new claims that make more sense in the application context. Based on the identity claim of the WCF-generated claim set, you can map the request to an application user ID and add the relevant identity and authorization information to a new claim set. The service operation would then simply reach into the WCF authorization context to retrieve the information in which it is interested, as illustrated in Figure 6 .
Figure 6 Claims Transformation (Click the image for a larger view)
Claims transformation is accomplished in an authorization policy. We have already used an authorization policy to create a custom principal, but this time it has a different purpose. Authorization policies can take part in the claims-generation process and run after the WCF internal claims generation is completed. That means the claims associated with the caller's credentials are already accessible to you.
You can query the existing claims for identity information and, based on that, create a new claim set that models your domain-specific claims. Then you can add it to the list of claim sets. This list will be used to create the authorization context before the request reaches the service operation.
There are three members on the IAuthorizationPolicy interface that you have to implement. Id returns a unique identifier for the policy (usually a GUID), and Issuer returns a claim set describing the issuer of the claims that this policy creates. The most important method, however, is Evaluate. This method receives an evaluation context, which basically represents the authorization context while it is still in the process of being built.
You get access to all currently generated claims via EvaluationContext.ClaimSets. Don't try to touch the ServiceSecurityContext from within the Evaluate method, as this will trigger the authorization policies again and you'll end up in an infinite loop. You can add a new claim set to the evaluation context by calling EvaluationContext.AddClaimSet—this claim set will become part of the authorization context later on.
Figure 7 shows a sample authorization policy that implements a common pattern. It first retrieves the identity claim of the first claim set. This claim is sent to a mapping component, which inspects the claim and returns an application user ID. After that the policy hits a data store using the ID to retrieve the information that should become part of the claim set. This information is then packaged as a new claim set and added to the evaluation context.
Figure 7 An Authorization Policy
class CustomerAuthorizationPolicy : IAuthorizationPolicy {
Guid _id = Guid.NewGuid();
// custom issuer claim set
ApplicationIssuerClaimSet _issuer = new ApplicationIssuerClaimSet();
public bool Evaluate(EvaluationContext evaluationContext,
ref object state) {
Claim id = evaluationContext.ClaimSets.FindIdentityClaim();
string userId = Map(id);
evaluationContext.AddClaimSet(this, new CustomerClaimSet(userId));
return true;
}
public ClaimSet Issuer {
get { return _issuer; }
}
public string Id {
get { return 'CustomerAuthorizationPolicy: ' + _id.ToString(); }
}
}
The final step is to add the authorization policy to the serviceAuthorization behavior configuration. You can add multiple policies, and they are invoked in the order you add them. This means you can write multi-step authorization policies where one policy relies on values added by a previous one. This is pretty powerful for more complex scenarios:
<serviceAuthorization>
<authorizationPolicies>
<add policyType='LeastPrivilege.CustomerAuthorizationPolicy, Service' />
<add policyType='some_other_policy' />
</authorizationPolicies>
</serviceAuthorization>
If you ran the previous code again to inspect the authorization context, you will see the new claim set and claims. The service operation would use similar code to check the claims for authorization (see Figure 8 ).
Figure 8 Checking Claims Authorization
public void PlaceOrder(Order order) {
int purchaseLimit = GetPurchaseLimit();
if (Order.Total > purchaseLimit) {
// do appropriate action
}
}
private int GetPurchaseLimit() {
AuthorizationContext context =
ServiceSecurityContext.Current.AuthorizationContext;
foreach (ClaimSet set in context.ClaimSets) {
foreach (Claim claim in set.FindClaims(
Constants.PurchaseLimitClaimType,
Rights.PossessProperty)) {
return int.Parse(claim.Resource.ToString());
}
}
throw new Exception('Claim not found');
}
The service authorization manager also works with claims-based authorization. WCF passes the operation context into the CheckAccessCore method. From there you can reach into the service security context that will give you access to the authorization context. This allows you to centralize certain authorization decisions in a single place.
Security Token Services
A security token service (STS) is a tool that allows further consolidation of security logic. The typical task of an STS is to authenticate users and create a security token that, in turn, can contain claims. Clients must first authenticate with the STS and then forward the returned token to the service with which the client wants to communicate.
Since the STS knows about that service (this information is part of the token request), it can do central authorization as well as pre-generate the claims on which the service relies. This way the claims transformation does not need to happen on the service endpoint at all but can be done centrally by the STS. This can dramatically streamline your security infrastructure when your system reaches a certain level of complexity.
A security token service is also an important infrastructure component when it comes to federating multiple trust domains. By establishing trust between several token services, you can exchange security tokens over the trust boundary that can be used by services.
WCF has automatic client/service-side support for the previous scenario as well as all the base classes needed to write an STS. But correctly implementing all the related WS-* specs is a complicated task. Instead, you should either buy a commercial STS or use a higher-level toolkit, such as Zermatt, to write a custom one. The upcoming version of Microsoft Active Directory Federation Services is designed to be a full-featured STS for WCF.
Send your questions and comments to sstation@microsoft.com .
Dominick Baier is a security consultant at thinktecture ( www.thinktecture.com ) in Germany. In addition, he is the security and WCF curriculum lead at DevelopMentor ( develop.com ), a developer security MVP, and the author of Developing More-Secure Microsoft ASP.NET 2.0 Applications . You can find his blog at leastprivilege.com .
Christian Weyer is cofounder and principal architect of thinktecture and has been modeling and implementing distributed applications with Java, COM, DCOM, COM+, Web Services, WCF, and other technologies. Get in touch with him at weblogs.thinktecture.com/cweyer/ .