In the first part of this series I described how you can secure your WCF service. The first part describes some of the most common security modes, how you can configure them and how you can add custom authentication.

Part 1 ends using the TransportWithMessageCredential security mode with a custom username/password validator. However, authorization in that scenario is not covered. That’s going to be the focus of this post.

Authorization in WCF

WCF allows us to restrict the access of operations using an attribute named PrincipalPermission , much like what you can do with ASP.NET MVC’s Authorize attribute. For example, this will restrict an operation to users with the Admin role:

[PrincipalPermission(SecurityAction.Demand, Role="Admin")] public void Remove(int userId) { //... }

Or if you want to specify alternative roles, for example Admin or Manager :

[PrincipalPermission(SecurityAction.Demand, Role="Admin")] [PrincipalPermission(SecurityAction.Demand, Role="Manager")] public void Remove(int userId) { //... }

If you want to enable more complicated scenarios, for example if you need to check if the user has two roles you need to test that programmatically. I’ll show you that in a second, but first I need to mention the ServiceSecurityContext class.

If you read the first post, and ran the gitub project with the examples you probably noticed that the test service is getting the identity of the user from two distinct sources:

ServiceSecurityContext.Current.PrimaryIdentity

Thread.CurrentPrincipal.Identity.Name

You probably also noticed that when the TransportWithMessageCredential security mode was used we were able to get the current user’s username from the ServiceSecurityContext but not from Thread.CurrentPrincipal . This is because we need to setup a custom authorization policy (because we are using custom authentication).

The ServiceSecurityContext contains the “identity” of the user that is logged in. I mentioned “identity” in quotes because these “identities” are instances of IIdentity . However, the fact that there’s an identity in ServiceSecurityContext is not enough to enable you to use the PrincipalPermission attribute, for example this won’t work even if your username is johndoe:

[PrincipalPermission(SecurityAction.Demand, Name="johndoe")] public void Remove(int userId) { //... }

You need to create an custom authorization policy and in it, set the Principal (this will become clear with an example).

Creating a custom IAuthorizationPolicy

The IAuthorizationPolicy interface is defined in the System.IdentityModel assembly (it’s the same that contains UserNamePasswordValidator ) and is:

public interface IAuthorizationPolicy : IAuthorizationComponent { ClaimSet Issuer { get; } bool Evaluate(EvaluationContext evaluationContext, ref object state); }

And IAuthorizationComponent just defines an string getter for a property named Id .

The Id needs to be unique, but can be whatever you want; I couldn’t find information about ClaimSet , all the examples I’ve found just implement it as return ClaimSet.System; , so if you have more information about this, please let me know in the comments, otherwise… just return ClaimSet.System .

The evaluate method, if it returns true it stops other authorization polices from being evaluated (there can be multiple authorization policies, that will become clear when we create the configuration to add our own configuration policy).

The three important questions that need to be answered regarding what you need to do in your implementation of this interface to enable authorization are:

Where is the custom authenticated user’s username? What do I have to do in the IAuthorizationPolicy ‘s Evaluate method to enable the use of the PrincipalPermission attribute in the service implementation? What do I have to do in the IAuthorizationPolicy ‘s Evaluate method to enable the use of Thread.CurrentPrincipal

1. Where is custom authenticated user’s username?

In the EvaluationContext instance that is passed in as a parameter to the Evaluate method there’s a property named Properties that is of type IDictionary<string, object> .

In that dictionary you’ll find a key named “Identities” which contains a List<IIdentity> instance. Usually it will only contain one element. If you are using a custom UserNamePasswordValidator (see Part 1) and the the class name is MyCustomUserNamePasswordValidator you’ll find one item in that list whose authentication type is “MyCustomUserNamePasswordValidator”.

Here’s how you can get to the instance of that IIdentity that represents the authenticated user, using MyCustomUserNamePasswordValidator :

var identity = (evaluationContext.Properties["Identities"] as List<IIdentity>).Single(i => i.AuthenticationType == "MyCustomUserNamePasswordValidator");

The username is here: identity.Name .

2. What do I have to do in the IAuthorizationPolicy’s Evaluate method to enable the use of the PrincipalPermission attribute in the service implementation?

You have to set a property named “Principal” in evaluationContext.Properties to an instance of an IPrincipal. The particular type of IPrincipal implementation you choose is not important, because all of them will implement IsInRole , and that’s what is important to enable the use of the PrincipalPermission attribute.

Here’s an example of creating a ClaimsPrincipal with the identity fetched in (1) and adding a role named Admin :

var claimsIdentity = new ClaimsIdentity(identity); claimsIdentity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, "Admin")); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); evaluationContext.Properties["Principal"] = claimsPrincipal;

3. What do I have to do in the IAuthorizationPolicy’s Evaluate method to enable the use of Thread.CurrentPrincipal

Just set it, for example:

Thread.CurrentPrincipal = claimsPrincipal;

And it will be available in your service’s operations. The reason for (3) is that if you want to enable scenarios where you want to check if your user has both the role “Admin” and “Manager” you can do this:

[PrincipalPermission(SecurityAction.Demand, Authenticated = true )] public void Remove(int userId) { var principal = Thread.CurrentPrincipal; if (!(principal.IsInRole("Admin") && principal.IsInRole("Manager"))) throw new System.ServiceModel.Security.SecurityAccessDeniedException("Insuficient privileges"); //... }

A custom authorization policy complete example

public class CustomAuthorizationPolicy : IAuthorizationPolicy { public bool Evaluate(EvaluationContext evaluationContext, ref object state) { var identity = (evaluationContext.Properties["Identities"] as List<IIdentity>).Single(i => i.AuthenticationType == "CustomUserNamePasswordValidator"); var claimsIdentity = new ClaimsIdentity(identity); claimsIdentity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, "Admin")); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); evaluationContext.Properties["Principal"] = claimsPrincipal; Thread.CurrentPrincipal = claimsPrincipal; return true; } public System.IdentityModel.Claims.ClaimSet Issuer { get { return ClaimSet.System; } } public string Id { get { return Guid.NewGuid().ToString(); } } }

Configuration for the new Authorization Policy

To add the new authorization policy we need to edit our WCF configuration and add it as part of a serviceBehavior , namely through serviceAuthorization , whose principalPermissionMode we must set to Custom , here’s an example for the configuration:

<behaviors> <serviceBehaviors> <behavior> ... <serviceAuthorization principalPermissionMode="Custom"> <authorizationPolicies> <add policyType="TheNamespace.CustomAuthorizationPolicy, TheAssemblyName"/> </authorizationPolicies> </serviceAuthorization> ... </behavior> </serviceBehaviors> </behaviors>

You can find a working example here: https://github.com/ruidfigueiredo/wcfsecuritysurvivalguide-part2-authorization

If you would want to see a more elaborate custom UserNamePasswordValidator and IAuthorizationPolicy , maybe using ASP.NET Identity, please let me know in the comments so I can decide if it’s worth dedicating some time to write about that.

It's only fair to share... Linkedin