Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C# 4.0

Custom Authentication and Authorization in WCF

Rate me:
Please Sign up or sign in to vote.
4.71/5 (42 votes)
19 Dec 2013CPOL5 min read 180.7K   59   20
Authentication and Authorization of WCF services using UserNamePasswordValidator and IAuthorizationPolicy.

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.

C#
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.

C#
[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.

C#
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.

XML
<behaviors> 
      <serviceBehaviors>
        <behavior name="customBehaviour">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/> 
        </behavior>
      </serviceBehaviors>
</behaviors>  

Next we configure <services> under <system.servicemodel>:

XML
<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:

C#
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>:

XML
<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:

XML
<behaviors> 
      <serviceBehaviors>
        <behavior name="customBehaviour">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/> 
          <!--Specify the Custom Authentication policy that will be used and add the policy location--> 
          <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.

C#
class AuthorizationPolicy : IAuthorizationPolicy 
{
    Guid _id = Guid.NewGuid();
    // this method gets called after the authentication stage
    public bool Evaluate(EvaluationContext evaluationContext, ref object state)
    {
        // get the authenticated client identity
        IIdentity client = GetClientIdentity(evaluationContext);
        // set the custom principal
        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.

C#
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.

XML
<behaviors> 
  <serviceBehaviors>
    <behavior name="customBehaviour">
      <serviceMetadata httpGetEnabled="true"/>
      <serviceDebug includeExceptionDetailInFaults="true"/>
      <!--Specify the Custom Authorization policy that will be used and add the policy location-->
      <serviceAuthorization principalPermissionMode="Custom">
        <authorizationPolicies>
          <add policyType="Service.AuthorizationPolicy, App_Code/Security" />
        </authorizationPolicies>
      </serviceAuthorization>
      <!--Specify the Custom Authentication policy that will be used and add the policy location--> 
      <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.

XML
<serviceCertificate findValue="STSTestCert"
                                storeLocation="LocalMachine"
                                x509FindType="FindBySubjectName"
                                storeName="My"/> 

The final configuration web.config will look like this:

XML
<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"/>

          <!--Specify the Custom Authorization policy that will be used and add the policy location-->
          <serviceAuthorization principalPermissionMode="Custom">
            <authorizationPolicies>
              <add policyType="Service.AuthorizationPolicy, App_Code/Security" />
            </authorizationPolicies>
          </serviceAuthorization>

          <!--Specify the Custom Authentication policy that will be used and add the policy location-->
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
                                    customUserNamePasswordValidatorType="Service.UserAuthentication,Service"/>

            <!--Specify the Certificate-->
            <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.

C#
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:

C#
[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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.
This is a Collaborative Group (No members)


Comments and Discussions

 
GeneralDont work Pin
Member 1477303615-Mar-20 6:52
Member 1477303615-Mar-20 6:52 
QuestionNeed the code Pin
Bhanu Pratap Verma4-Jun-18 0:51
Bhanu Pratap Verma4-Jun-18 0:51 
QuestionReally Simple and straight to the point Pin
Member 1193722426-Sep-17 23:19
Member 1193722426-Sep-17 23:19 
GeneralMy vote of 5 Pin
Dan Randolph17-May-17 7:20
Dan Randolph17-May-17 7:20 
PraiseWorks great and article was easy to follow Pin
Dan Randolph17-May-17 5:43
Dan Randolph17-May-17 5:43 
QuestionHelp with the certificate Pin
max31726-Jun-15 8:50
max31726-Jun-15 8:50 
QuestionHi my firend Pin
vasjos4-Dec-14 5:06
vasjos4-Dec-14 5:06 
QuestionHow to set UserId after validate and use in service Pin
shahnaitik29-Nov-14 2:41
shahnaitik29-Nov-14 2:41 
GeneralMy vote of 1 Pin
hasan22224-Oct-14 10:28
hasan22224-Oct-14 10:28 
should have attached source code. Everything is not covered.
GeneralMy vote of 1 Pin
Yasin HINISLIOGLU31-Aug-14 1:18
Yasin HINISLIOGLU31-Aug-14 1:18 
SuggestionSource Code Example Pin
cesarsan711-Aug-14 12:17
cesarsan711-Aug-14 12:17 
AnswerModifications to the source, Pin
Madhav A Shenoy9-Aug-14 18:50
Madhav A Shenoy9-Aug-14 18:50 
PraiseRe: Modifications to the source, Pin
Dan Randolph17-May-17 7:33
Dan Randolph17-May-17 7:33 
QuestionSample Code Pin
nim2741-Aug-14 10:06
nim2741-Aug-14 10:06 
Questionpublic bool IsInRole(string role): name _identity/_roles does not exist. Pin
B K Singh 218723-Jul-14 0:54
professionalB K Singh 218723-Jul-14 0:54 
GeneralMy vote of 1 Pin
Member 998910122-Apr-14 20:53
Member 998910122-Apr-14 20:53 
QuestionNo App_Code Directory and problems with custom princpal Pin
CiaranCoyle6-Feb-14 4:28
CiaranCoyle6-Feb-14 4:28 
AnswerRe: No App_Code Directory and problems with custom princpal Pin
B K Singh 218723-Jul-14 0:48
professionalB K Singh 218723-Jul-14 0:48 
GeneralRe: No App_Code Directory and problems with custom princpal Pin
CiaranCoyle10-Jun-16 2:56
CiaranCoyle10-Jun-16 2:56 
SuggestionSource Missing Pin
abuzer5-Feb-14 22:07
abuzer5-Feb-14 22:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.