Introduction
In this article I would like to dive into Authentication and Authorization of WCF services using
UserNamePasswordValidator
and IAuthorizationPolicy
.
The prerequisites for this article are as follows:
You should have installed the following:
- Windows XP or a Window 7
- IIS installed
- .NET Framework 3.5 or 4.0
Creation of a WCF Service
To create a WCF Service, you have to define the Service Contract that has to be exposed to the client. An example of
a service contract interface for a service is shown below.
namespace Service
{
[ServiceContract]
public interface IService1
{
[OperationContract]
string SayHello(string value);
[OperationContract]
string GetData(int value);
[OperationContract]
string UpdatePatientData(PatientData PatientInfo);
}
}
Here the interface needs to be marked as ServiceContract
and the methods should be marked with
the OperationContract
attribute. If the methods within
the interface have parameters to be passed, the parameter types should be either serializable or
data contracts. Also, I have a DataContract
class called PatientData
in
the IService1.cs file.
[DataContract]
public class PatientData
{
[DataMember]
public int Age { get; set; }
[DataMember]
public string Email { get; set; }
[DataMember]
public string Gender { get; set; }
[DataMember]
public string Name { get; set; }
}
Next we need to implement IService1
in Service1.svc.cs.
public class Service1 : IService1
{
public string SayHello(string value)
{
return "Hello:" + value;
}
public string GetData(int value)
{
return string.Format("You entered: {0}", value);
}
public string UpdatePatientData(PatientData PatientInfo)
{
return string.Format("You entered: {0} , {1} , {2} , {3}",
PatientInfo.Name, PatientInfo.Age, PatientInfo.Gender, PatientInfo.Email);
}
}
Next we need to configure the web.config containing the address, binding, and endpoint of the service. Open web.config,
under the <system.servicemodel>
tag in the <behaviours>
section, we will add the behavior name.
<behaviors>
<serviceBehaviors>
<behavior name="customBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
Next we configure <services>
under <system.servicemodel>
:
<services>
<service name="Service.Service1" behaviorConfiguration="customBehaviour">
<endpoint address=""
binding="wsHttpBinding"
contract ="Service.IService1">
</endpoint>
<endpoint contract="IMetadataExchange"
binding="mexHttpBinding"
address="mex" />
</service>
</services>
Once you have configured
the service, try to access the service.svc file or host it, you should be able
to view the service in the browser and be able to access the WSDL.
Implementing UserNamePasswordValidator
Till now we have only done the basics of WCF, i.e., created the interface, implemented the interface, and configured the
web.config and made sure the service is up. Next we will see how we can make use of UserNamePasswordValidator
to authenticate
the user. Right click on the solution and add a new class,
provide a valid/proper name for the class. In my case I have named the class UserAuthentication.cs. Open the class and inherit the
UserNamePasswordValidator
abstract
class and then we will override the Validate
method which accepts two
parameters: username
and the password
. The figure below shows this:
public class UserAuthentication:UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
try
{
if (userName == "test" && password == "test123")
{
Console.WriteLine("Authentic User");
}
}
catch (Exception ex)
{
throw new FaultException("Unknown Username or Incorrect Password");
}
}
}
Here in the Validate
method, I am just doing a check on some dummy user and printing it on the
console or authenticating the user. You can have your own implementation like
checking the user against your database or any custom logic. You may be
wondering how or who will pass the username and the password, we need not
worry about that right now as it’s the client that is going to access the
service that will send both the username and password when it makes a request to the
service operation. We will be seeing this in the later part.
Once we have done this,
there are a few changes to be done in the web.config of the service. Open the configuration
file and start making changes to it. We will have to make a few changes to wsHttpBinding
.
Inside <system.servicemodel>
, add <bindings>
:
<bindings>
<wsHttpBinding>
<binding name="ServiceBinding">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
Since we have already
implemented the authentication logic in the UserAuthentication.cs file,
we need to tell our service that there is custom authentication implemented, and how
do we do this? Again, config file comes in to picture. Inside <serviceBehaviours>
, add markup as shown below:
<behaviors>
<serviceBehaviors>
<behavior name="customBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="Service.UserAuthentication,Service"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
Implementing IAuthorizationPolicy
In this section, we will implement the authorization rules for the authenticated user.
Right click on the App_Code folder and create a new folder and provide a valid name.
In this folder, create a new class file and name it AuthorizationPolicy.cs. Open the class and inherit the interface
IAuthorizationPolicy
. This interface
is under System.IdentityModel.Policy
. Implement the interface methods.
Right click on IAuthorizationPolicy
and implement.
This interface has one important method called Evaluate
. This method
checks for the authenticated user’s identity.
Note: For more information on this, please refer to the article
http://www.codeproject.com/Articles/33872/Custom-Authorization-in-WCF.
class AuthorizationPolicy : IAuthorizationPolicy
{
Guid _id = Guid.NewGuid();
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
IIdentity client = GetClientIdentity(evaluationContext);
evaluationContext.Properties["Principal"] = new CustomPrincipal(client);
return true;
}
private IIdentity GetClientIdentity(EvaluationContext evaluationContext)
{
object obj;
if (!evaluationContext.Properties.TryGetValue("Identities", out obj))
throw new Exception("No Identity found");
IList<IIdentity> identities = obj as IList<IIdentity>;
if (identities == null || identities.Count <= 0)
throw new Exception("No Identity found");
return identities[0];
}
}
Now after getting the client's identity, we have to set the roles for the user. This is done by implementing one more interface called
IPrincipal
.
Add a new class in the App_Code -> Security folder and name
it CustomPrincipal.cs. Inherit the interface IPrincipal
in this class, this interface in under the namespace System.Security.Principal
. Right click
and implement this interface. This interface has an important method called
IsInRole
where we can set the roles for the logged in user.
public bool IsInRole(string role)
{
if (_identity.Name == "test")
_roles = new string[1] { "ADMIN" };
else
_roles = new string[1] { "USER" };
return _roles.Contains(role);
}
Since we have already implemented the authorization logic in the AuthorizationPolicy.cs file, we need to tell our service that there is custom
authorization implemented. Please open the configuration file.
<behaviors>
<serviceBehaviors>
<behavior name="customBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceAuthorization principalPermissionMode="Custom">
<authorizationPolicies>
<add policyType="Service.AuthorizationPolicy, App_Code/Security" />
</authorizationPolicies>
</serviceAuthorization>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="Service.UserAuthentication,Service"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
We are almost done, but if you want the solution to work, then we also need to install a
certificate in our machine and provide the certificate inside <serviceCredentials>
. I have already created and installed a certificate called
STSTestCert
on my machine.
<serviceCertificate findValue="STSTestCert"
storeLocation="LocalMachine"
x509FindType="FindBySubjectName"
storeName="My"/>
The final configuration web.config will look like this:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<services>
<service name="Service.Service1" behaviorConfiguration="customBehaviour">
<endpoint address=""
binding="wsHttpBinding"
contract ="Service.IService1"
bindingConfiguration="ServiceBinding"
behaviorConfiguration="MyEndPointBehavior" >
</endpoint>
<endpoint contract="IMetadataExchange"
binding="mexHttpBinding"
address="mex" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="ServiceBinding">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="MyEndPointBehavior">
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="customBehaviour">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceAuthorization principalPermissionMode="Custom">
<authorizationPolicies>
<add policyType="Service.AuthorizationPolicy, App_Code/Security" />
</authorizationPolicies>
</serviceAuthorization>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="Service.UserAuthentication,Service"/>
<serviceCertificate findValue="STSTestCert"
storeLocation="LocalMachine"
x509FindType="FindBySubjectName"
storeName="My"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Creating a client
To your solution, add a new Console Client and add the Service Reference. Create a proxy and try to access the service operations.
Since we have implemented the authentication logic we need to pass the user credentials to access the
service. At this point of time, we can access both the service operations once the user is authenticated.
class Program
{
static void Main(string[] args)
{
ServiceReference1.Service1Client serviceProxy = new ServiceReference1.Service1Client();
serviceProxy.ClientCredentials.UserName.UserName = "test";
serviceProxy.ClientCredentials.UserName.Password = "test123";
PatientData objData = new PatientData();
objData.Name = "test";
objData.Gender = "Male";
objData.Email = "v@g.com";
objData.Age = 20;
Console.WriteLine(serviceProxy.UpdatePatientData(objData));
Console.ReadLine();
string message= serviceProxy.GetData(5);
Console.WriteLine(message);
Console.ReadLine();
}
}
We have also implemented the authorization logic, but here the client is able to access both the
methods, so how do we and how can we restrict the access on the methods? This is quite simple as we have to make use of
the PrincipalPermission
attribute on top
of the method in Service1.svc.cs. Make changes as shown below:
[PrincipalPermission(SecurityAction.Demand, Role = "ADMIN")]
public string GetData(int value)
{
try
{
return string.Format("You entered: {0}", value);
}
catch (Exception exp)
{
MyFaultException theFault = new MyFaultException();
theFault.Reason = "Some Error " + exp.Message.ToString();
throw new FaultException<MyFaultException>(theFault);
}
}
[PrincipalPermission(SecurityAction.Demand, Role = "ADMIN")]
public string UpdatePatientData(PatientData PatientInfo)
{
try
{
return string.Format("You entered: {0} , {1} , {2} , {3}",
PatientInfo.Name, PatientInfo.Age, PatientInfo.Gender, PatientInfo.Email);
}
catch(Exception exp)
{
MyFaultException theFault = new MyFaultException();
theFault.Reason = "Some Error " + exp.Message.ToString();
throw new FaultException<MyFaultException>(theFault);
}
}
So even if the user is authenticated, I have configured the PrincipalPermission
attribute to accept the user with the role of admin. To access the operation the user needs
to be an admin of the machine. Here the role can also be set to Groups. You need to create the Group on your machine and add users to this group
so that these operations are accessible to users in this particular group. I have also implemented Fault Exception so that any exception caught on
the service side
is sent to the client application.
Hope this article helps you get a good idea about implementing security in WCF by using
UserNamePasswordValidator
for authentication and IAuthorizationPolicy
for authorization.