SYSK 374: Role Based Authorization for WCF Services through Configuration Files
Traditionally, to implement role based authorization in .NET you would use PrincipalPermission attributes, e.g.:
[PrincipalPermission(SecurityAction.Demand, Name = "YourDomain\\User1", Role = "YourDomain\\Role1")]
public void MethodX(...)
{
. . .
}
In my opinion, there are two problems with this approach:
1. At development time you need to make a decision on which roles are allowed/denied
2. In case of WCF services (at least on .NET 3.5), there doesn’t appear to be a way to add the PrincipalPermission attribute at a class level and you end up adding this attribute to every method (it appears that, at the construction time, currentPrincipal.Identity.IsAuthenticated is returning false, so no IsInRole checks are performed).
Here is how I address this requirement:
1. In WCF services config file (e.g. web.config, if you’re using IIS as a host), add the following configuration:
<configuration>
<configSections>
< sectionname = "authorizationRules"type=" WCFExtensions.AuthorizationRulesSection, WCFExtensions" />
</configSections>
. . .
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="YourBehaviorNameHere">
. . .
< serviceAuthorizationserviceAuthorizationManagerType = "WCFExtensions.AuthorizationManager, WCFExtensions" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
< authorizationRules >
< services >
< servicename = "Service1.svc"defaultAccess="deny"overrideUsers=" YourDomain\Me "overrideRoles="YourDomain\Role1,YourDomain\Role2">
< operationname = "IService1/Method1"allowUsers="YourDomain\User2"allowRoles=""denyUsers=""denyRoles="" />
</ service >
</ services >
</ authorizationRules >
</configuration>
The idea is simple – at a service level, you can allow or deny access as a default behavior, and override that rule by denying/allowing only certain users and/or roles. This authorization rule will apply to all methods (operations) unless you override that behavior by adding <operation> element.
Here is an example of what I mean… Let’s say you want to allow access to Service1 only to one group – MyAppUsers, but, for a very sensitive Delete operation, you only want MyAppAdmins to be able to perform it. To implement this requirement, you’d configure it as follows:
<service name="Service1.svc" defaultAccess="deny" overrideRoles="MyAppUsers">
<operation name="IService1/Delete" denyRoles="MyAppUsers" allowRoles="MyAppAdmins" />
</service>
2. To implement the authorization rules at run time, create a class library WCFExtensions and add the two classes – AuthorizationManager.cs and ConfigurationEx.cs with the implementation listed below:
using System;
using System.Security.Principal;
using System.ServiceModel;
namespace WCFExtensions
{
// TODO: add SEH and logging
public class AuthorizationManager : ServiceAuthorizationManager
{
public override bool CheckAccess(OperationContext operationContext, ref System.ServiceModel.Channels.Message message)
{
bool result = false;
// Allow all calls to the mex endpoint
if ("IMetadataExchange" == operationContext.EndpointDispatcher.ContractName)
{
result = true;
}
else
{
// We require Windows authentication
if (operationContext.ServiceSecurityContext.WindowsIdentity != null)
{
// Caller identity
WindowsIdentity caller = operationContext.ServiceSecurityContext.WindowsIdentity;
// Get service and operation
Uri uri = operationContext.IncomingMessageHeaders.To;
string service = uri.PathAndQuery;
string operation = new Uri(operationContext.IncomingMessageHeaders.Action).AbsolutePath.Substring(1);
object o = System.Configuration.ConfigurationManager.GetSection("authorizationRules");
if (o == null)
{
// If the section doesn't exist, let everybody in...
result = true;
}
else if (o is WCFExtensions.AuthorizationRulesSection)
{
result = WCFExtensions.AuthorizationRulesSection.CheckAccess(service, operation, caller);
}
else
{
throw new Exception("authorizationRules section in configuration file is of the wrong type");
}
}
}
return result;
}
}
}
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Security.Principal;
namespace WCFExtensions
{
public enum DefaultAccessMode
{
allow,
deny
}
// TODO: add SEH and logging
public class AuthorizationRulesSection : ConfigurationSection
{
private static readonly AuthorizationRulesSection Instance = (AuthorizationRulesSection)System.Configuration.ConfigurationManager.GetSection("authorizationRules");
private readonly ConfigurationProperty _services =
new ConfigurationProperty("services", typeof(ServiceElementCollection));
[ConfigurationProperty("services", IsDefaultCollection = true)]
public ServiceElementCollection Services
{
get
{
return (ServiceElementCollection)base[_services];
}
}
public static bool CheckAccess(string serviceName, string operationName, WindowsIdentity caller)
{
bool result = false;
// Find the service
ServiceElementCollection services = Instance.Services;
// Try to match taking into account the number of Uri fragments in the config file
ServiceElement serviceElem = null;
foreach (ServiceElement item in services)
{
if (serviceName.EndsWith(item.Name))
{
serviceElem = item;
break;
}
}
if (serviceElem != null)
{
AuthRules rules = new AuthRules(serviceElem.DefaultAccess, serviceElem.OverrideUsers, serviceElem.OverrideRoles);
OperationElement opElem = serviceElem.Operations[operationName];
if (opElem != null)
{
rules.AddRule(opElem.AllowUsers, opElem.AllowRoles, opElem.DenyUsers, opElem.DenyRoles);
}
bool? evalResult = rules.CheckAccess(caller);
if (evalResult == null)
{
// No rule => use defaults
result = (serviceElem.DefaultAccess == DefaultAccessMode.allow);
}
else
{
result = (bool)evalResult;
}
}
else
{
// If there is no restriction, allow access
result = true;
}
return result;
}
}
[ConfigurationCollection(typeof(ServiceElement), AddItemName="service")]
public class ServiceElementCollection : ConfigurationElementCollection
{
public ServiceElementCollection()
: base(StringComparer.OrdinalIgnoreCase)
{
}
protected override ConfigurationElement CreateNewElement()
{
return new ServiceElement();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((ServiceElement)element).Name;
}
public new ServiceElement this[string name]
{
get
{
// Force the get by key, not index
object key = name;
return (ServiceElement)base.BaseGet(key);
}
}
public ServiceElement this[int index]
{
get
{
return (ServiceElement)base.BaseGet(index);
}
}
}
public class ServiceElement : ConfigurationElementCollection
{
private readonly ConfigurationProperty _name = new ConfigurationProperty("name", typeof(string), String.Empty, null, null, ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey);
private readonly ConfigurationProperty _defaultAccess = new ConfigurationProperty("defaultAccess", typeof(DefaultAccessMode));
private readonly ConfigurationProperty _overrideUsers = new ConfigurationProperty("overrideUsers", typeof(string));
private readonly ConfigurationProperty _overrideRoles = new ConfigurationProperty("overrideRoles", typeof(string));
private readonly ConfigurationProperty _operations = new ConfigurationProperty(null, typeof(OperationElementCollection), null, ConfigurationPropertyOptions.IsDefaultCollection);
[ConfigurationProperty("name", Options = ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey, DefaultValue = "")]
public string Name
{
get { return (string)base[_name]; }
set { base[_name] = value; }
}
[ConfigurationProperty("defaultAccess", DefaultValue = DefaultAccessMode.allow)]
public DefaultAccessMode DefaultAccess
{
get { return (DefaultAccessMode)base[_defaultAccess]; }
set { base[_defaultAccess] = value; }
}
[ConfigurationProperty("overrideUsers", DefaultValue = "")]
public string OverrideUsers
{
get { return (string)base[_overrideUsers]; }
set { base[_overrideUsers] = value; }
}
[ConfigurationProperty("overrideRoles", DefaultValue = "")]
public string OverrideRoles
{
get { return (string)base[_overrideRoles]; }
set { base[_overrideRoles] = value; }
}
[ConfigurationProperty("", IsDefaultCollection = true)]
public OperationElementCollection Operations
{
get
{
return (OperationElementCollection)base[_operations];
}
}
protected override ConfigurationElement CreateNewElement()
{
return new OperationElement();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((OperationElement)element).Name;
}
}
[ConfigurationCollection(typeof(OperationElement), AddItemName = "operation")]
public class OperationElementCollection : ConfigurationElementCollection
{
public OperationElementCollection()
: base(StringComparer.OrdinalIgnoreCase)
{
}
protected override ConfigurationElement CreateNewElement()
{
return new OperationElement();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((OperationElement)element).Name;
}
public new OperationElement this[string name]
{
get
{
// Force the get by key, not index
object key = name;
return (OperationElement)base.BaseGet(key);
}
}
public OperationElement this[int index]
{
get
{
return (OperationElement)base.BaseGet(index);
}
}
}
public class OperationElement : ConfigurationElement
{
private readonly ConfigurationProperty _name = new ConfigurationProperty("name", typeof(string), String.Empty, null, null, ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey);
private readonly ConfigurationProperty _allowUsers = new ConfigurationProperty("allowUsers", typeof(string));
private readonly ConfigurationProperty _allowRoles = new ConfigurationProperty("allowRoles", typeof(string));
private readonly ConfigurationProperty _denyUsers = new ConfigurationProperty("denyUsers", typeof(string));
private readonly ConfigurationProperty _denyRoles = new ConfigurationProperty("denyRoles", typeof(string));
[ConfigurationProperty("name", Options = ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey, DefaultValue = "")]
public string Name
{
get { return (string)base[_name]; }
set { base[_name] = value; }
}
[ConfigurationProperty("allowUsers", DefaultValue = "")]
public string AllowUsers
{
get { return (string)base[_allowUsers]; }
set { base[_allowUsers] = value; }
}
[ConfigurationProperty("allowRoles", DefaultValue = "")]
public string AllowRoles
{
get { return (string)base[_allowRoles]; }
set { base[_allowRoles] = value; }
}
[ConfigurationProperty("denyUsers", DefaultValue = "")]
public string DenyUsers
{
get { return (string)base[_denyUsers]; }
set { base[_denyUsers] = value; }
}
[ConfigurationProperty("denyRoles", DefaultValue = "")]
public string DenyRoles
{
get { return (string)base[_denyRoles]; }
set { base[_denyRoles] = value; }
}
}
internal class AuthRules
{
private List<string>
_allowUsers = new List<string>(),
_denyUsers = new List<string>(),
_allowRoles = new List<string>(),
_denyRoles = new List<string>();
internal AuthRules(DefaultAccessMode mode, string overrideUsers, string overrideRoles)
{
if (mode == DefaultAccessMode.allow)
{
_denyUsers.AddRange(overrideUsers.ToLower().Split(','));
_denyRoles.AddRange(overrideRoles.ToLower().Split(','));
}
else
{
_allowUsers.AddRange(overrideUsers.ToLower().Split(','));
_allowRoles.AddRange(overrideRoles.ToLower().Split(','));
}
}
internal void AddRule(string allowUsers, string allowRoles, string denyUsers, string denyRoles)
{
AddRule(allowUsers, _allowUsers, _denyUsers);
AddRule(allowRoles, _allowRoles, _denyRoles);
AddRule(denyUsers, _denyUsers, _allowUsers);
AddRule(denyRoles, _denyRoles, _allowRoles);
}
private void AddRule(string rule, List<string> addToList, List<string> removeFromList)
{
if (string.IsNullOrEmpty(rule) == false)
{
foreach (string item in rule.ToLower().Split(','))
{
addToList.Add(item);
if (removeFromList.Contains(item))
removeFromList.Remove(item);
}
}
}
internal bool? CheckAccess(WindowsIdentity caller)
{
bool? result = null; // undetermined
// Users take precedence over roles; deny takes precedence over allow
string userName = caller.Name.ToLower();
if (_denyUsers.Contains(userName))
{
result = false;
}
else if (_allowUsers.Contains(userName))
{
result = true;
}
else
{
// When it comes to groups, if user belongs to one of the allowed groups, access is granted
WindowsPrincipal callerPrincipal = new WindowsPrincipal(caller);
foreach (string group in _allowRoles)
{
if (callerPrincipal.IsInRole(group))
{
result = true;
break;
}
}
// Not in the allowedRoles => look at denied roles
if (result == null)
{
// Make sure the caller is not in one of the denied groups
foreach (string group in _denyRoles)
{
if (callerPrincipal.IsInRole(group))
{
result = false;
break;
}
}
}
}
return result;
}
}
}
3. Compile and deploy WCFExtensions library to the bin folder of your WCF Services
As always, feel free to use/modify the code for your project’s needs… but if you do, I’d appreciate if you leave your comments/feedback…
Comments
- Anonymous
September 19, 2010
I was looking for something similar to implement in my services, will evaluate and see if i can extend it for my requirements. Thanks a lot for sharing...