There are loads of articles on the web about how to implement windows or certificate authentication but very few that tell you how to actually use the authentication mechanism to restrict access to a WCF service to a particular group of users. One fantastic exception is this blog by Richard Seroter, on which much of this article is based. The major difference is that I also implement Claims based authentication for use with a federated service end point as well as bring the code samples up to date.
So you’ve got a WCF service working with either Windows or Federated security, but now what? How do we authorize particular users/groups? Unfortunately there is no out of the box solution in BizTalk (at least not up to 2010) so you have to create your own custom behavior extension. The good news is that you only need to write this component once and you can then re-use it across all of your BizTalk WCF services.
In Visual Studio create a new Class called BizTalkCustomServiceAuthManager.cs in an existing or newly created generic project (I call mine <CompanyName>.BizTalk.CommonArtifacts.Components which also holds lots of my other common classes). Add a reference to System.configuration, System.IdentiyModel and System.ServiceModel. Create 3 properties, m_group which will be the AD Group or the Claim Value to search for, the m_claimtype which will be the name of the Claim to search for (Claims only) and m_authenticationType which will be an enum allowing you to select either Windows or Claims based authentication in the Receive Port configuration. This could easily be extended to handle another type of authentication mechanism if you wanted.
public class BizTalkCustomServiceAuthManager : ServiceAuthorizationManager
{
private string m_group;
private string m_claimtype;
private CustomServiceAuthenticationType m_authenticationType;
///
<summary> /// Claims or Windows Authentication
/// </summary>
public enum CustomServiceAuthenticationType
{
WINDOWS=1,
CLAIMS=2
};
///
<summary> /// Initializes a new instance of the class.
/// </summary>
///The group.
///Type of the authentication.
public BizTalkCustomServiceAuthManager(string group, CustomServiceAuthenticationType authenticationType, string claimType)
{
this.m_group = group;
this.m_authenticationType = authenticationType;
this.m_claimtype = claimType;
}
}
Now add the following block of code which will form the basis of the core authorization logic for either Windows or Claims authentication depending on what is chosen. Note that I’ve added a lot of Debug lines so you can view what is going on when we start testing the configuration.
/// <summary>
/// Checks authorization for the given operation context based on default policy evaluation.
/// </summary>
/// <param name="operationContext">The <see cref="T:System.ServiceModel.OperationContext"/> for the current authorization request.</param>
/// <returns>
/// true if access is granted; otherwise, false. The default is true.
/// </returns>
protected override bool CheckAccessCore(OperationContext operationContext)
{
//check that basic access is ok before checking our custom conditions
if (!base.CheckAccessCore(operationContext))
{
return false;
}
if (this.m_authenticationType == CustomServiceAuthenticationType.CLAIMS)
{
return performClaimsAuthentication(operationContext);
}
if (this.m_authenticationType == CustomServiceAuthenticationType.WINDOWS)
{
return performWindowsAuthentication(operationContext);
}
else
{
System.Diagnostics.Trace.WriteLine("No Authentication Type Selected in BizTalk binding ");
return false;
}
}
/// <summary>
/// Performs the claims authentication.
/// </summary>
/// <param name="operationContext">The operation context.</param>
/// <returns></returns>
public bool performWindowsAuthentication(OperationContext operationContext)
{
//print out inbound identities recorded by WCF
System.Diagnostics.Trace.WriteLine("Primary Identity is " +
operationContext.ServiceSecurityContext.PrimaryIdentity.Name);
System.Diagnostics.Trace.WriteLine("Windows Identity is " +
operationContext.ServiceSecurityContext.WindowsIdentity.Name);
//create Windows principal object from inbound Windows identity
WindowsPrincipal p = new WindowsPrincipal(operationContext.ServiceSecurityContext.WindowsIdentity);
//check user in role
string[] windowsGroups = this.m_group.Split(',');
foreach (string windowsGroup in windowsGroups)
{
bool isAdmin = p.IsInRole(windowsGroup);
if (isAdmin)
{
System.Diagnostics.Trace.WriteLine("User is in role. Security Accepted " +
windowsGroup);
return true;
}
else
{
System.Diagnostics.Trace.WriteLine("User is not in role '" +
windowsGroup + "'");
}
}
System.Diagnostics.Trace.WriteLine("Security Rejected Request.");
return false;
}
/// <summary>
/// Performs the claims authentication.
/// </summary>
/// <param name="operationContext">The operation context.</param>
/// <returns></returns>
public bool performClaimsAuthentication(OperationContext operationContext)
{
string[] claimValues = this.m_group.Split(',');
foreach (ClaimSet claimset in operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets)
{
foreach (Claim claim in claimset.FindClaims(this.m_claimtype, Rights.PossessProperty))
{
//Add to headers
System.Diagnostics.Trace.WriteLine("Current Claim is " +
claim.ClaimType);
System.Diagnostics.Trace.WriteLine("Current Claim Resource is " +
claim.Resource.ToString());
foreach (string claimValue in claimValues)
{
if (claim.Resource.ToString() == claimValue)
{
System.Diagnostics.Trace.WriteLine(String.Format("User is in claim {0} Security Accepted ",claimValue));
return true;
}
}
}
}
System.Diagnostics.Trace.WriteLine("User is not claim. Security Denied " +
this.m_group);
return false;
}
Now add a new class called BizTalkCustomServiceBehavior.cs and extend IServiceBehavior. This doesn’t really do very much but is necessary.
public class BizTalkCustomServiceBehavior : IServiceBehavior
{
private string m_group;
private .BizTalk.CommonArtifacts.Components.BizTalkCustomServiceAuthManager.CustomServiceAuthenticationType m_authenticationType;
private string m_claimtype;
///
<summary> /// Initializes a new instance of the class.
/// </summary>
///The group.
///Type of the authentication.
public BizTalkCustomServiceBehavior(string group, .BizTalk.CommonArtifacts.Components.BizTalkCustomServiceAuthManager.CustomServiceAuthenticationType authenticationType, string claimType)
{
this.m_group = group;
this.m_authenticationType = authenticationType;
this.m_claimtype = claimType;
}
///
<summary> /// Provides the ability to change run-time property values or insert custom extension objects such as error handlers, message or parameter interceptors, security extensions, and other custom extension objects.
/// </summary>
///The service description.
///The host that is currently being built.
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
ServiceAuthorizationBehavior authBehavior =
serviceDescription.Behaviors.Find();
//pass in Windows Group set during config setup
authBehavior.ServiceAuthorizationManager = new BizTalkCustomServiceAuthManager(this.m_group, this.m_authenticationType, this.m_claimtype);
((IServiceBehavior)authBehavior).ApplyDispatchBehavior(serviceDescription, serviceHostBase);
}
/// <summary> /// Provides the ability to pass custom data to binding elements to support the contract implementation.
///The service description of the service.
///The host of the service.
///The service endpoints.
///Custom objects to which binding elements have access.
public void AddBindingParameters(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
Collection endpoints,
BindingParameterCollection bindingParameters) {
}
public void Validate(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase
)
{
}
}
And finally add the Behaviour Element where we define the custom properties that can be set in the receive port properties.
public class BizTalkCustomBehaviorElement : BehaviorExtensionElement
{
///
/// Want custom config property to show up in the BizTalk receive location
///
[ConfigurationProperty("group", IsRequired = false, DefaultValue = "")]
public string Group
{
get { return (string)base["group"]; }
set { base["group"] = value; }
}
///
<summary> /// Gets or sets the type of the auth.
/// </summary>
///
/// The type of the auth.
///
[ConfigurationProperty("authenticationtype", IsRequired = false, DefaultValue = CompanyName.BizTalk.CommonArtifacts.Components.BizTalkCustomServiceAuthManager.CustomServiceAuthenticationType.WINDOWS)]
public CompanyName.BizTalk.CommonArtifacts.Components.BizTalkCustomServiceAuthManager.CustomServiceAuthenticationType AuthType
{
get { return (CompanyName.BizTalk.CommonArtifacts.Components.BizTalkCustomServiceAuthManager.CustomServiceAuthenticationType)base["authenticationtype"]; }
set { base["authenticationtype"] = value; }
}
///
<summary> /// Gets or sets the type of the claim.
/// </summary>
///
/// The type of the claim.
///
[ConfigurationProperty("claimtype", IsRequired = false, DefaultValue = "")]
public string ClaimType
{
get { return (string)base["claimtype"]; }
set { base["claimtype"] = value; }
}
///
<summary> /// Creates a behavior extension based on the current configuration settings.
/// </summary>
///
/// The behavior extension.
///
protected override object CreateBehavior()
{
return new BizTalkCustomServiceBehavior(Group, AuthType,ClaimType);
}
///
<summary> /// Gets the type of behavior.
/// </summary>
/// A .
public override Type BehaviorType
{
get
{
return typeof(BizTalkCustomServiceBehavior);
}
}
}
Now build your project and add it into the GAC using a command like this "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\NETFX 4.0 Tools\x64\gacutil.exe" /if "D:\Projects\Biztalk\CompanyName.CommonArtifacts\CompanyName.CommonArtifacts.Components\bin\Debug\CompanyName.BizTalk.CommonArtifacts.Components.dll"
Next you need to open your machine.config – I know it sounds a bit scary but it’s the only way. Mine is located in C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config but it depends on what version of .NET you are using and also whether you are running x86 or 64 bit applications.
Search for the behavior extensions tag <behaviorExtensions> and at the bottom add a link to your new class that has been added to the GAC. Note that if you aren’t sure what this should look like you can view the .dll meta info using something like gacutil.exe -l CompanyName.BizTalk.CommonArtifacts.Components
<add name="CustomAuthBehavior" type="CompanyName.BizTalk.CommonArtifacts.Components.BizTalkCustomBehaviorElement, CompanyName.BizTalk.CommonArtifacts.Components, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6dbf238400666825" />
</behaviorExtensions>
Almost done. Now restart your BizTalk Host Instances and close and re-open the BizTalk Administration Console. Open the properties for any Windows or Federated binding WCF-Custom receive port (note that it does have to be WCF-Custom) and click Configure. In the Behavior tab, right click ServiceBehavior and select “Add extension…”. You should see your new Service Behavior class appearing there – if it isn’t check you edited the correct machine.config (note you will need to edit the machione.config within the relevant Framework64 directory as well if you are running 64bit OS) and that it is definitely referencing the right assembly.
![SelectCustomBehavior]()
To configure access to a windows AD group, select Windows authenticationType and specify the AD group (including the domain). Note that you can add multiple groups by separating with commas.
![CustomBehaviorWindows]()
To use the Claims authentication you also need to specify which Claim to search on. The Group will relate to the value of that claim, e.g. in this example the claim type is a member of a SharePoint 2010 site http://schemas.company.com/identity/claims/2010/07/teammember and the value is the name of the site itself, e.g. “Team A”. Again you can specify multiple values by separating the group parameter with commas.
![CustomBehaviorClaims]()
Now restart IIS as well and put some test cases through. If the user fails you will receive a Service Authorization fault. The Debug output might look something like this.
![CustomBehaviorClaimsDebugOutput]()
So perhaps not the most straightforward of solutions and I really think Microsoft should have bundled this functionality out the box. But the pain is only had once and at least you get total flexibility for how the authorization is performed.