Tuesday, April 22, 2014

WCF - Service Authentication


This post will help you to implement Message Credential Security on WCF Services over HTTP/SOAP 1.1.

I have seen in many services implementations that require authentication for consume service's operations, providing the user's credential in message body, as a class's property. Eg:

   [DataContract]
    public class PersonType
    {
        #region Primitive Properties
 
        [DataMember]
        public int PersonTypeID
        {
            get;
            set;
        }
 
        [DataMember]
        public string PersonTypeName
        {
            get;
            set;
        }
 
        [DataMember]
        public string UserName
        {
            get;
            set;
        }
 
        [DataMember]
        public string Password
        {
            get;
            set;
        }
    }

The consumer should define the UserName and Password properties to invoke a service operation. Each service operation need to read this properties and validate then:

public PersonType AddPersonType(PersonType personType)
{
    if(!AuthenticateUser(personType.UserName, personType.Password))
        throw new Exception("Login Failed")
 
    //Method implementation
}

This will work, but is not a SOA best practice. The canonical model will contain your domain model mixed with technical attributes.

Microsoft WCF provides in System.IdentityModel namespace, classes to automate and standardize according of the SOAP 1.1 security specifications.

Create your own UserIdentity class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Principal;
 
namespace Contoso.Security.Principal
{
    /// <summary>
    /// Defines the basic atributtes of user identity.
    /// </summary>
    public class UserIdentity : IIdentity
    {
        #region Constructor
 
        /// <summary>
        /// Creates a new instance of UserIdentity class.
        /// </summary>
        /// <param name="name">The user name.</param>
        /// <param name="login">The user login.</param>
        /// <param name="ipAdress">The user ip.</param>
        public UserIdentity(int id, string name, string login, string ipAddress)
        {
            this.ID = id;
            this.Name = name;
            this.Login = login;
            this.IPAddress = ipAddress;
        }
 
        #endregion
 
        #region Properties
 
        public int ID
        {
            get;
            private set;
        }
 
        /// <summary>
        /// Indicates if user is athenticated.
        /// </summary>
        public bool IsAuthenticated
        {
            get { return true; }
        }
 
        /// <summary>
        /// Gets the user name.
        /// </summary>
        public string Name
        {
            get;
            private set;
        }
 
        /// <summary>
        /// Gets the user login name.
        /// </summary>
        public string Login
        {
            get;
            private set;
        }
 
        /// <summary>
        /// Client Address
        /// </summary>
        public string IPAddress
        {
            get;
            set;
        }     
 
        public string AuthenticationType
        {
           get { return "ContosoSecurity"; }
        }
 
        #endregion
    }
}

Now, you need create a class that implements IPrincipal interface:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Principal;
using System.Threading;
using System.Diagnostics;
using System.Security;
 
namespace Contoso.Security.Principal
{
    /// <summary>
    /// Represents service principal.
    /// </summary>
    public class ContosoPrincipal : IPrincipal
    {
        #region Fields
 
        private WindowsPrincipal _impersonationPrincipal;
        private UserIdentity _identity;
 
        #endregion
 
        #region Constructors
 
        /// <summary>
        /// Default constructor
        /// </summary>
        public ContosoPrincipal()
        {
        }
 
        /// <summary>
        /// Initializes an instance of the <see cref="Contoso.Security.ContosoPrincipal"/> class.
        /// </summary>
        /// <param name="identity">User identity.</param>
        /// <param name="impersonatePrincipal">Principal to be impersonated.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="identity"/>is null</exception>
        public ContosoPrincipal(UserIdentity identity, WindowsPrincipal impersonatePrincipal)
        {
            if (identity == null)
                throw new ArgumentNullException("identity");
 
            _impersonationPrincipal = impersonatePrincipal;
            _identity = identity;
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Gets the impersonate identity.
        /// </summary>
        public WindowsIdentity ImpersonationIdentity
        {
            get
            {
                WindowsIdentity identity = null;
 
                if (_impersonationPrincipal != null)
                {
                    identity = _impersonationPrincipal.Identity as WindowsIdentity;
                }
 
                return identity;
            }
        }
 
        /// <summary>
        /// Gets the identity of the current principal.
        /// </summary>
        public IIdentity Identity
        {
            get { return _identity; }
        }
 
        /// <summary>
        /// Windows principal to be used in case of impersonation.
        /// </summary>
        public WindowsPrincipal ImpersionationPrincipal
        {
            get { return _impersonationPrincipal; }
        }
 
        #endregion
 
        #region Public Methods
 
        /// <summary>
        /// Determines whether the current principal belongs to the specified role.
        /// </summary>
        /// <param name="role">The name of the role for which to check membership.</param>
        /// <returns><c>true</c> if the current principal is a member of the specified role; otherwise, <c>false</c>.</returns>
        public bool IsInRole(string role)
        {
            UserIdentity userIdentity = GetCurrent().Identity as UserIdentity;
 
            //Implement your own security and policy class to manage the access
            return new SecurityManager().CheckAuthorization(userIdentity.ID, role);
        }
 
        /// <summary>
        /// Returns the current princpal thread as a ContosoPrincipal class instance.
        /// </summary>
        public static ContosoPrincipal GetCurrent()
        {
            ContosoPrincipal principal = Thread.CurrentPrincipal as ContosoPrincipal;
 
            return principal;
        }
 
        #endregion
 
    }
}

Create the Service Authentication class, responsible for receive the UserName and Password to validate:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IdentityModel.Selectors;
using System.ServiceModel;
using System.Diagnostics;
using System.Threading;
using System.Security.Principal;
using Microsoft.Win32;
using Contoso.Security.Principal;
 
namespace Contoso.Security.ServiceModel
{
    /// <summary>
    /// Validates user credentials of the running services.
    /// </summary>
    public class ServiceAuthentication : UserNamePasswordValidator
    {
        #region Public Methods
 
        /// <summary>
        /// Validates the specified username and password across the AD.
        /// </summary>
        /// <param name="userName">User name to be checked.</param>
        /// <param name="password">Password to be checked.</param>
        public override void Validate(string userName, string password)
        {
            try
            {
                if (string.IsNullOrEmpty(userName))
                    throw new ArgumentNullException("userName");
 
                //Implement your own security and policy class to manage the access
                SecurityManager security = new SecurityManager();
 
                //throw an exception when login fails 
                UserIdentity identity = security.ValidateUser(userName, password);
 
                ContosoPrincipal principal = new ContosoPrincipal(identity, Thread.CurrentPrincipal as WindowsPrincipal);
                Thread.CurrentPrincipal = principal;
            }
            catch (Exception ex)
            {
                throw new FaultException(new FaultReason("Login Failed"));
            }
        }
 
        #endregion
 
    }
}

Service Authorization Policy:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.IdentityModel.Policy;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Channels;
using Contoso.Security.Principal;
 
namespace Contoso.Security.ServiceModel
{
    /// <summary>
    /// Defines a set of rules for authorizing a user.
    /// </summary>
    public class ServiceAuthorizationPolicy : IAuthorizationPolicy
    {
        #region Constructors
 
        /// <summary>
        /// Creates a new instance of ServiceAuthorizationPolicy class
        /// </summary>
        public ServiceAuthorizationPolicy()
        {
            this.Id = Guid.NewGuid().ToString();
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Gets a claim set that represents the issuer of the authorization policy.
        /// </summary>
        public System.IdentityModel.Claims.ClaimSet Issuer
        {
            get { return System.IdentityModel.Claims.ClaimSet.System; }
        }
 
        /// <summary>
        /// Gets a string that identifies this authorization component.
        /// </summary>
        public string Id
        {
            get;
            private set;
        }
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Evaluates whether a user meets the requirements for this authorization policy.
        /// </summary>
        /// <param name="evaluationContext">
        /// An System.IdentityModel.Policy.EvaluationContext that contains the claim
        /// set that the authorization policy evaluates.
        /// <param name="state">
        /// A System.Object, passed by reference that represents the custom state for this authorization policy.
        /// </param>
        /// <returns>
        /// false if the System.IdentityModel.Policy.IAuthorizationPolicy.Evaluate(System.IdentityModel.Policy.EvaluationContext,System.Object@)
        /// method for this authorization policy must be called if additional claims are added by other authorization policies 
        /// to evaluationContext; otherwise, true to state no additional evaluation is required by this authorization policy.
        /// </returns>
        public bool Evaluate(EvaluationContext evaluationContext, ref object state)
        {
                ContosoPrincipal principal = System.Threading.Thread.CurrentPrincipal as ContosoPrincipal;
 
                if (principal != null)
                {
                    UserIdentity _userIdentity = principal.Identity as UserIdentity;
 
                    _userIdentity.IPAddress = GetIPAddress();
 
                    evaluationContext.Properties["Principal"] = new ContosoPrincipal(_userIdentity, principal.ImpersionationPrincipal);
                }
 
                return true;
            }            
        }
 
        private string GetIPAddress()
        {
            OperationContext context = OperationContext.Current;
            MessageProperties messageProperties = context.IncomingMessageProperties;
            RemoteEndpointMessageProperty endpointProperty = messageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
 
            return endpointProperty.Address;
        }
 
        #endregion
    }
}

To finalize server-side implementation, remains only the Web.config's setup. We will need to configure the Bindings, Behaviors and the Service's Endpoints:


  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" name="HttpsBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:01:00" sendTimeout="00:01:00">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
          <security mode="TransportWithMessageCredential">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </basicHttpBinding>
     
    </bindings>
 
    <behaviors>
      <serviceBehaviors>
        <behavior name="SecureBehavior">
          <dataContractSerializer maxItemsInObjectGraph="2147483647" />
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
          <serviceThrottling maxConcurrentCalls="5000" />
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Contoso.Security.ServiceModel.ServiceAuthentication, Contoso.Security.ServiceModel" />
          </serviceCredentials>
          <serviceAuthorization principalPermissionMode="Custom">
            <authorizationPolicies>
              <add policyType="Contoso.Security.ServiceModel.ServiceAuthorizationPolicy, Contoso.Security.ServiceModel" />
            </authorizationPolicies>
          </serviceAuthorization>
        </behavior>    
      </serviceBehaviors>
 
    </behaviors>
 
    <services>
      <service behaviorConfiguration="SecureBehavior" name="Contoso.Global.Services.Business.BrandService">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="HttpsBinding" contract="Contoso.Global.Services.Contract.IBrandContract" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="/Global/v1/Brand.svc" />
          </baseAddresses>
        </host>
      </service>
    </services>
 
    <serviceHostingEnvironment>
      <serviceActivations>
        <add relativeAddress="Global/v1/Brand.svc" service="Contoso.Global.Services.Business.BrandService" />
      </serviceActivations>
    </serviceHostingEnvironment>
  </system.serviceModel>

Your service will able to be consumed. Remember, this comunication must be over the SSL protocol and credential's informations, such username and password, must be encrypted.

.NET consumers need to set credential informations to invoke the service:



using (BrandService.BrandClient client = new BrandService.BrandClient())
{
    client.ClientCredentials.UserName.UserName = "CONTOSO_USER";
    client.ClientCredentials.UserName.Password = "BNQ1bQWap4Qh2GslOyJojFB3dLV0FGDkfGY13yTfFV3O7eHCWHL04h9P==";
 
    client.AddBrand(new Entity.Global.Brand());
}

You can invoke the service sending a message like below:

<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v1="http://www.contoso.com/global/v1/">
  <soapenv:Header>
    <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <o:UsernameToken>
        <o:Username>CONTOSO_USER</o:Username>
        <o:Password>BNQ1bQWap4Qh2GslOyJojFB3dLV0FGDkfGY13yTfFV3O7eHCWHL04h9P==</o:Password>
      </o:UsernameToken>
    </o:Security>
  </soapenv:Header>
  <soapenv:Body>
    <v1:addBrand>
     ...
    </v1:addBrand>
  </soapenv:Body>
</soapenv:Envelope>

No comments:

Post a Comment