Introduction
Windows Communication Foundation provides tons of methods to authenticate users' check for authorization based on
the service type that it's quite confusing to implement simple form based authentication and role based authorization for WCF REST 4.0.
Note: This article assumes that the WCF REST service is hosted with
the ASP.NET application and shares the same web.config. Make sure that Form Authentication is enabled in
the web.config file.
Background
There are many ways to authenticate and authorize a user in a WCF Service, but in this example, the authentication cookie will already be created by
the login page and that will be used by subsequent requests made to the REST service for authorization.
Using the Code
A typical way to authorize a user for a specific role is to use the Principal Permission attribute. Something like this:
WebGet(UriTemplate = "")]
[PrincipalPermission(SecurityAction.Demand, Role="Admin")]
public List<SampleItem> GetCollection(){}
But even though after the user is authenticated using Membership provider and HTTPContext.Current.User.Identity
and the context is available at service level, the principal permission attribute always throws a security exception.
The reason for that is the principal permission attribute checks for System.Threading.Thread.CurrentPrincipal.Identity
and not for
the HTTPContext
Identity.
To solve this problem, we have to create a Custom Principal and Authorization Policy for
the WCF Service. Then this policy will be hooked with the WCF REST Service using ServiceBehaviour
.
Custom Principal
Here is the code for the custom principal:
public class CustomPrincipal: IPrincipal
{
private IIdentity _identity;
public IIdentity Identity
{
get
{
return _identity;
}
}
public CustomPrincipal(IIdentity identity)
{
_identity = identity;
}
public bool IsInRole(string role)
{
return Roles.IsUserInRole(role);
}
}
Here the ASP.NET Membership Role provider is used to verify if the user is in a particular role or not. We can have our custom implementation that does not use
the Membership provider.
Authorization Policy
Now create an Authorization policy that sets the Custom Principal to the evaluation context:
public class AuthorizationPolicy : IAuthorizationPolicy
{
string id = Guid.NewGuid().ToString();
public string Id
{
get { return this.id; }
}
public System.IdentityModel.Claims.ClaimSet Issuer
{
get { return System.IdentityModel.Claims.ClaimSet.System; }
}
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
IIdentity client = HttpContext.Current.User.Identity;
evaluationContext.Properties["Principal"] = new CustomPrincipal(client);
return true;
}
}
If you look closely, the custom principal is created using the HTTPContext
Identity that was created after
the user is authenticated using membership provider and the authentication cookie is set after validating the user. Something like this:
FormsAuthentication.SetAuthCookie(username, false);
Attach Authorization Policy to WCF
This can be done by creating a service behavior in the web.config file. But here, I have created
a custom service behavior by implementing IServiceBehavior
and attaching the authorization policy to it.
[AttributeUsage(AttributeTargets.Class)]
public class SecurityBehaviorAttribute : Attribute, IServiceBehavior
{
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
policies.Add(new AuthorizationPolicy());
serviceHostBase.Authorization.ExternalAuthorizationPolicies =
policies.AsReadOnly();
ServiceAuthorizationBehavior bh =
serviceDescription.Behaviors.Find<ServiceAuthorizationBehavior>();
if (bh != null)
{
bh.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
}
else
throw new NotSupportedException();
}
public void AddBindingParameters(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { }
public void Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase) { }
}
Here, ServiceAuthorizationBehavior PrincipalPermissionMode
is set to Custom
and
the Authorization
policy is added to the servicehost
.
Service Code
Make sure that the service behavior is added as an attribute to the service
class.
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[SecurityBehavior]
public class Service1 {
[WebGet(UriTemplate = "")]
[PrincipalPermission(SecurityAction.Demand, Role="Admin")]
public List<SampleItem> GetCollection()
{
var value = System.Web.HttpContext.Current.User.Identity.IsAuthenticated;
return new List<SampleItem>() { new SampleItem()
{ Id = 1, StringValue = "Hello" } };
}
}
That's all. Now we can add the PrincipalPermission
attribute to any web method and authorize
the user for a specific role. We can also implement a custom PrincipalPermission
attribute to control the granularity of authorization.
Note: We can also create an Authentication
service to validate
the user name password and create an authentication cookie after validation. Here, the assumption is that WCF REST is hosted with
the Web application and therefore shares the context.
Let me know if there is any better way to achieve the same thing without providing user name and password at each request.