Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>I found a solution to my problem, however, it turned out to be a lot nastier than I expected.</p> <p>Basically, to achieve both Transport and Message credential checking you need to define a custom binding. (I found info to this effect <a href="http://social.msdn.microsoft.com/forums/en-US/wcf/thread/2c168952-7d67-4e6f-810a-6dd11d4dc55a/" rel="nofollow noreferrer">here</a>).</p> <p>I found the easiest way to do this is to continue doing your configuration in the XML, but at runtime copy and slightly modify the netTcp binding from the XML configuration. There is literally one switch you need to enable. Here's the code on the service side and on the client side:</p> <p><strong>Service Side</strong></p> <pre><code>ServiceHost businessHost = new ServiceHost(typeof(DHTestBusinessService)); ServiceEndpoint endpoint = businessHost.Description.Endpoints[0]; BindingElementCollection bindingElements = endpoint.Binding.CreateBindingElements(); SslStreamSecurityBindingElement sslElement = bindingElements.Find&lt;SslStreamSecurityBindingElement&gt;(); sslElement.RequireClientCertificate = true; //Turn on client certificate validation CustomBinding newBinding = new CustomBinding(bindingElements); NetTcpBinding oldBinding = (NetTcpBinding)endpoint.Binding; newBinding.Namespace = oldBinding.Namespace; endpoint.Binding = newBinding; </code></pre> <p><strong>Client Side</strong></p> <pre><code>DHTestBusinessServiceClient client = new DHTestBusinessServiceClient(); ServiceEndpoint endpoint = client.Endpoint; BindingElementCollection bindingElements = endpoint.Binding.CreateBindingElements(); SslStreamSecurityBindingElement sslElement = bindingElements.Find&lt;SslStreamSecurityBindingElement&gt;(); sslElement.RequireClientCertificate = true; //Turn on client certificate validation CustomBinding newBinding = new CustomBinding(bindingElements); NetTcpBinding oldBinding = (NetTcpBinding)endpoint.Binding; newBinding.Namespace = oldBinding.Namespace; endpoint.Binding = newBinding; </code></pre> <p>You'd think that'd be it, but you'd be wrong! :) This is where it gets extra lame. I was attributing my concrete service methods with PrincipalPermission to restrict access based on roles of the service user like this:</p> <pre><code>[PrincipalPermission(SecurityAction.Demand, Role = "StandardUser")] </code></pre> <p>This started failing as soon as I applied the above changes. The reason was because the</p> <pre><code>OperationContext.Current.ServiceSecurityContext.PrimaryIdentity </code></pre> <p>was ending up being an unknown, username-less, unauthenticated IIdentity. This was because there are actually <em>two</em> identities representing the user: one for the X509 certificate used to authenticate over Transport, and one for the username and password credentials used to authenticate at Message level. When I reverse engineered the WCF binaries to see why it wasn't giving me my PrimaryIdentity I found that it has an explicit line of code that causes it to return that empty IIdentity if it finds more than one IIdentity. I guess it's because it's got no way to figure out which one is the <em>primary</em> one.</p> <p>This means using the PrincipalPermission attribute is out the window. Instead, I wrote a method to mimic its functionality that <em>can</em> deal with multiple IIdentities:</p> <pre><code>private void AssertPermissions(IEnumerable&lt;string&gt; rolesDemanded) { IList&lt;IIdentity&gt; identities = OperationContext.Current.ServiceSecurityContext.AuthorizationContext.Properties["Identities"] as IList&lt;IIdentity&gt;; if (identities == null) throw new SecurityException("Unauthenticated access. No identities provided."); foreach (IIdentity identity in identities) { if (identity.IsAuthenticated == false) throw new SecurityException("Unauthenticated identity: " + identity.Name); } IIdentity usernameIdentity = identities.Where(id =&gt; id.GetType().Equals(typeof(GenericIdentity))).SingleOrDefault(); string[] userRoles = Roles.GetRolesForUser(usernameIdentity.Name); foreach (string demandedRole in rolesDemanded) { if (userRoles.Contains(demandedRole) == false) throw new SecurityException("Access denied: authorisation failure."); } } </code></pre> <p>It's not pretty (especially the way I detect the username/password credential IIdentity), but it works! Now, at the top of my service methods I need to call it like this:</p> <pre><code>AssertPermissions(new [] {"StandardUser"}); </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload