WCF username authorization

When using UserName instead of the default Windows authentication, the idea of using Custom username validator can only do the authentication part, while the authorization always needs more info besides the username, usually it’s a list of roles. We could create a CustomAuthorizationPolicy : IAuthorizationPolicy, then fill out the evalute method something like this:


         <behavior name="customAuthPolicy">
            <serviceMetadata httpGetEnabled="true"/>
            <serviceDebug includeExceptionDetailInFaults="true"/>

            <serviceAuthorization principalPermissionMode ="Custom" >
                <authorizationPolicies>
                  <add policyType="Premotion.Services.CustomAuthorizationPolicy, App_Code" />
                </authorizationPolicies>
            </serviceAuthorization>
            <serviceCredentials>
                <serviceCertificate findValue="MyServerCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="My" />
                <userNameAuthentication userNamePasswordValidationMode="Custom"
                                        customUserNamePasswordValidatorType="Premotion.Services.UsernameValidator, App_Code" />
            </serviceCredentials>
        </behavior>


    public class CustomAuthorizationPolicy : IAuthorizationPolicy
    {
        public string Id
        {
            get { return new Guid().ToString(); }
        }

        public bool Evaluate(EvaluationContext evaluationContext, ref object state)
        {
            // get the authenticated client identity
            IIdentity client = GetClientIdentity(evaluationContext);

            List<string> roles = new List<string>();
            if (client.Name == "frank") roles.Add("Admins");

            evaluationContext.Properties["Principal"] = new CustomPrincipal(client, roles.ToArray());

            return true;
        }

        public ClaimSet Issuer
        {
            get { return ClaimSet.System; }
        }

        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 (obj == null || identities.Count <= 0)
                throw new Exception("No Identity found");

            return identities&#91;0&#93;;
        }
    }

&#91;/sourcecode&#93;

Not only the relection code to get identity from  evalution context,  to create our own CustomPriciple and CustomIdentiy class is also very annoying, even it's simple, but, can we make it simpler. What we need just a list of roles based on the username.

&#91;sourcecode language="CSharp"&#93;
    /// <summary>
    /// Make sure to generate the test certificate
    /// C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin
    /// makecert.exe -sr LocalMachine -ss My -a sha1 -n CN=MyServerCert -sky exchange –pe
    /// </summary>
    [TestFixture]
    public class UsernameWcfSpecs
    {
        private ServiceHost _host;
        private const string _url = "http://localhost:9000/TestUsernameAuthentication";

        private void SetupWcfHostTakingWindowsLogin()
        {
            _host = new ServiceHost(typeof (TestService));

            var binding = new WSHttpBinding();
            binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows; // default

            _host.AddServiceEndpoint(typeof (ITestService), binding, _url);
            _host.Open();
            Console.WriteLine("wcf service started.");
        }

        private void SetupWcfHostTakingUsernameButValidateItAgainstWindows()
        {
            _host = new ServiceHost(typeof (TestService));

            var binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Message; // default

            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; // default is windows

            // protect message sent from client and certificate host
            _host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My,
                                                                X509FindType.FindBySubjectName, "MyServerCert");

            _host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =
                UserNamePasswordValidationMode.Windows; // default

            _host.AddServiceEndpoint(typeof (ITestService), binding, _url);

            _host.Open();
            Console.WriteLine("wcf service started.");
        }

        private void SetupWcfHostTakingUsernameAndDoCustomValidation()
        {
            _host = new ServiceHost(typeof (TestService));

            var binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Message; // default

            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; // default is windows

            // protect message sent from client and certificate host
            _host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My,
                                                                X509FindType.FindBySubjectName, "MyServerCert");

            _host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =
                UserNamePasswordValidationMode.Custom; // default is windows

            _host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.None; // default is UseWindowGroups
            _host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator =
                new CustomUsernameValidator(new MyActiveDirectoryService());

            _host.AddServiceEndpoint(typeof (ITestService), binding, _url);

            _host.Open();
            Console.WriteLine("wcf service started.");
        }

        private void SetupWcfHostUsingUsernameAuthCustomValidationWithAuthorization()
        {
            _host = new ServiceHost(typeof (TestService));

            var binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Message; // default

            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; // default is windows

            // protect message sent from client and certificate host
            _host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My,
                                                                X509FindType.FindBySubjectName, "MyServerCert");

            _host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =
                UserNamePasswordValidationMode.Custom; // default is windows
            var policies = new List<IAuthorizationPolicy>();
            policies.Add(new CustomAuthorizationPolicy(new MyActiveDirectoryService()));
            _host.Authorization.ExternalAuthorizationPolicies = policies.AsReadOnly();
            _host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom; // default is UseWindowGroups

            _host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator =
                new CustomUsernameValidator(new MyActiveDirectoryService());

            _host.AddServiceEndpoint(typeof (ITestService), binding, _url);

            _host.Open();
            Console.WriteLine("wcf service started.");
        }

        private ChannelFactory<ITestService> _channelFactory;
        private bool _success;

        [TearDown]
        public void CleanUpChannelFacotry()
        {
            if (_success)
            {
                _channelFactory.Close();
            }
            else
            {
                _channelFactory.Abort();
            }

            _host.Close();
            Console.WriteLine("Wcf host stopped.");
        }

        [Test]
        public void should_connect_to_host_taking_windows_auth()
        {
            SetupWcfHostTakingWindowsLogin();

            var binding = new WSHttpBinding();

            _channelFactory = new ChannelFactory<ITestService>(
                binding,
                new EndpointAddress(_url));

            ITestService proxy = _channelFactory.CreateChannel();

            proxy.Ping();
            _success = true;
        }

        [Test]
        public void should_connect_to_host_taking_username_auth_windows_login()
        {
            SetupWcfHostTakingUsernameButValidateItAgainstWindows();

            var binding = new WSHttpBinding();
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            binding.Security.Mode = SecurityMode.Message;

            // usually certificate should be same as the domain name of service host, so set identity at client to bypass this check.
            _channelFactory = new ChannelFactory<ITestService>(
                binding,
                new EndpointAddress(new Uri(_url), EndpointIdentity.CreateDnsIdentity("MyServerCert")));

            // Ignore certificate, call could be redirected to a malicious service (through client address resolving)
            _channelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode =
                X509CertificateValidationMode.None;

            _channelFactory.Credentials.UserName.UserName = "guest";
            _channelFactory.Credentials.UserName.Password = "hello";

            ITestService proxy = _channelFactory.CreateChannel();

            proxy.Ping();
            _success = true;
        }

        [Test]
        public void should_connect_to_host_takeing_username_validation()
        {
            SetupWcfHostTakingUsernameAndDoCutomValidation();

            var binding = new WSHttpBinding();

            binding.Security.Mode = SecurityMode.Message;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;

            // usually certificate should be same as the domain name of service host, so set identity at client to bypass this check.
            _channelFactory = new ChannelFactory<ITestService>(
                binding,
                new EndpointAddress(new Uri(_url), EndpointIdentity.CreateDnsIdentity("MyServerCert")));

            // Ignore certificate, call could be redirected to a malicious service (through client address resolving)
            _channelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode =
                X509CertificateValidationMode.None;

            _channelFactory.Credentials.UserName.UserName = "guest";
            _channelFactory.Credentials.UserName.Password = "hello";

            ITestService proxy = _channelFactory.CreateChannel();

            proxy.Ping();
            bool noAccess = false;
            try
            {
                proxy.CmisUsersOnly();
            }
            catch (SecurityAccessDeniedException)
            {
                noAccess = true;
            }
            Assert.That(noAccess);

            _success = true;
        }

        [Test]
        public void should_connect_to_host_takeing_username_validation_with_authorization()
        {
            SetupWcfHostUsingUsernameAuthCustomValidationWithAuthorization();

            var binding = new WSHttpBinding();

            binding.Security.Mode = SecurityMode.Message;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;

            // usually certificate should be same as the domain name of service host, so set identity at client to bypass this check.
            _channelFactory = new ChannelFactory<ITestService>(
                binding,
                new EndpointAddress(new Uri(_url), EndpointIdentity.CreateDnsIdentity("MyServerCert")));

            // Ignore certificate, call could be redirected to a malicious service (through client address resolving)
            _channelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode =
                X509CertificateValidationMode.None;

            _channelFactory.Credentials.UserName.UserName = "guest";
            _channelFactory.Credentials.UserName.Password = "hello";

            ITestService proxy = _channelFactory.CreateChannel();

            proxy.Ping();
            proxy.CmisUsersOnly();
            _success = true;
        }

AspSqlMembership

In fact, MS provide a default Role-based AspSqlMembership Provider for WCF, with a pre-defined database and whole set of membership provider / role manager classes. Fortunately, We can easily override the classes to let WCF to go against our own credential store.


        <behavior name="AglcSqlMembership">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceAuthorization principalPermissionMode ="UseAspNetRoles" />
          <serviceCredentials>
            <serviceCertificate findValue="MyServerCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="My" />
            <userNameAuthentication userNamePasswordValidationMode="MembershipProvider" />
          </serviceCredentials>
        </behavior>

...

    <system.web>
    <membership defaultProvider ="MySqlMembershipProvider" >
<providers>
        <add name ="MySqlMembershipProvider"
             type ="Premotion.Services.MySqlMembershipProvider"
             connectionStringName ="WcfUsers"
          />
      </providers>
     </membership>

    <roleManager enabled="true" defaultProvider ="MySqlRoleManager">
<providers>
        <add name ="MySqlRoleManager"
             type ="Premotion.Services.MySqlRoleManager"
             connectionStringName ="WcfUsers"
             />
      </providers>
    </roleManager>
  </system.web>
  <connectionStrings>
    <add name="WcfUsers"  connectionString ="data souce=.\SQLEXPRESS; Integrated Security=SSPI; Initial Catalog=WcfUsers" />
  </connectionStrings>


    public class MySqlMembershipProvider : SqlMembershipProvider
    {
        public override bool ValidateUser(string username, string password)
        {
            // check if the user is not test
            if (username == "test1" && password == "test1") return true;
            return false;
        }

    public class MySqlRoleManager : SqlRoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            if (username == "test1" && roleName == "Admin") return true;
            return false;
        }
    }  

  }

public class CoreService: ICoreServiceContract
{
        [PrincipalPermission(SecurityAction.Demand, Role="Admin")]
        public void AdminsOnly()
        {

        }
}

public string Hello()
{
   OperationContext context = OperationContext.Current;

   MessageProperties messageProperties = context.IncomingMessageProperties;

   RemoteEndpointMessageProperty endpointProperty =
     messageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;

   return string.Format("Hello {0}! Your IP address is {1} and your port is {2}",
      ServiceSecurityContext.Current.PrimaryIdentity.Name, endpointProperty.Address, endpointProperty.Port);
   // If it's Windows authorization
   // return "How are you ! " + Thread.CurrentPrincipal.Identity.Name;
 }

I had to add a connectionStringName in those xml definition part, even I’m not using it at all.

Some interesting differences,

security mode UserNamePassword Validation Mode Authorization PrincipalPermissionMode Authentication Type principle type
Windows Kerberos WindowsPrinciple
UserName Windows anyone but Custom NTLM WindowsPrinciple
UserName Custom (must apply CutomAuthorziationPolicy, otherwise the Thread.CurrentPrincipal is not authenticated while ServiceSecurityContext is ) Custom CustomUsernameValidator
UserName MembershipProvider UseAspNetRoles MembershipProviderValidator GenricPrinciple

Updates: The WsHttpBinding using Windows auth which sendng Kerberos auth will fail on Vista client, the error message is “The message or signature supplied for verification has been altered”. Microsoft admit it’s a bug.

Advertisements

4 thoughts on “WCF username authorization

  1. Pingback: WCF PrincipalPermission « maonet technotes

  2. Pingback: Why doesn't my new Windows Installer record exercise morally over an comparison chronicle of a MSI? - Adrien

  3. A despair is the place inside of a tooth that remaining elements as soon as tooth rot is uprooted.
    Plaque, a dreary, sticky film of microbes that usually structures on teeth, is one
    particular of the essential driver of tooth rot. The purpose we
    brush and floss consistently is to uproot plaque, on the grounds that microbes in plaque responds with sugar in the nourishments we eat to generate acids that can assault and debilitate tooth lacquer the tough, defensive blanket on our enamel.
    Disintegrating polish leaves the tooth unprotected, getting into account holes
    to create all the more efficiently.

    On the gnawing surfaces of the enamel. This occurs when plaque will get
    to be caught in the notches or cleft of the enamel.
    This is most normal in kids, in light of the simple
    fact that they usually skip these ranges when brushing.

    This transpires when plaque is still left to create on the hard-to-achieve surfaces.
    These areas can’t be arrived at by a toothbrush by yourself and may be defenseless to rot on the off likelihood that you don’t floss or clear in between your enamel, customarily.

    This is typical in the celebration that you have endured gum subsidence or
    bone misfortune, often linked with gum illness, or periodontitis.
    It’s additionally a lot more regular to find out rot right here as you get far more set up if
    your gums retreat. On the off opportunity that plaque is
    remaining to create on the uncovered bases of the teeth, then a gap can speedily
    generate in mild of the simple fact that roots don’t
    have the identical challenging veneer coating.

    Thankfully, there are many factors you can do to aid maintain the creation of a pit.
    Attempt this dental specialist proposed oral forethought plan:

    Use toothpaste or mouthwash containing fluoride, an executor that
    fortifies enamel and aides anticipate pits

    Consider soon after a sound eating methodology
    and dodge sugary sustenances and drinks

    Program normal visits with your dental practitioner, rather
    practically at typical intervals, for a schedule cleaning and examination

    Preserve in mind to modify your toothbrush when it looks worn, or at normal intervals, on the grounds that the a lot more recent the
    abounds, the more plaque the brush has the capability uproot

    You should to anticipate looking at your dental practitioner twice a
    calendar year for checkups. Although checkups can change,
    your dental practitioner will doubtlessly inspect your tooth and gums
    for any apparent concerns, like the advancement of a
    melancholy. He or she might moreover get X-beams to verify there
    aren’t any concerns that aren’t but visible. Your dental professional may possibly additionally skillfully cleanse
    your teeth to expel solidified plaque from previously mentioned and underneath
    the gum line, and glow them to expel plaque and stains
    from the tooth floor. Also remember your dental professional can likewise give grasp path on brushing
    and flossing.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s