C# – Catch-22 prevents streamed TCP WCF service securable by WIF; ruining the Christmas, mental health

.net-4.5cnettcpbindingwcfwif

I have a requirement to secure a streamed WCF net.tcp service endpoint using WIF. It should authenticate incoming calls against our token server. The service is streamed because it is designed to transfer large amounts of data n stuff.

This appears to be impossible. And if I can't get around the catch, my Christmas will be ruined and I'll drink myself to death in a gutter while merry shoppers step over my slowly cooling body. Totes serious, you guys.

Why is this impossible? Here's the Catch-22.

On the client, I need to create a channel with the GenericXmlSecurityToken I get from our token server. No problemo.

// people around here hate the Framework Design Guidelines.
var token = Authentication.Current._Token;
var service = base.ChannelFactory.CreateChannelWithIssuedToken(token);
return service.Derp();

Did I say "no problemo"? Problemo. In fact, NullReferenceException style problemo.

"Bro, " I asked the Framework, "do you even null check?" The Framework was silent, so I disassembled and found that

((IChannel)(object)tChannel).
    GetProperty<ChannelParameterCollection>().
    Add(federatedClientCredentialsParameter);

was the source of the exception, and that the GetProperty call was returning null. So, WTF? Turns out that if I turn on Message security and set the client credential type to IssuedToken then this property now exists in the ClientFactory (protip: There is no "SetProperty" equivalent in IChannel, the bastard).

<binding name="OMGWTFLOL22" transferMode="Streamed" >
    <security mode="Message">
        <message clientCredentialType="IssuedToken"/>
    </security>
</binding>

Sweet. No more NREs. However, now my client is faulted at birth (still love him, tho). Digging through WCF diagnostics (protip: make your worst enemies do this after crushing them and driving them before you but right before enjoying the lamentations of their women and children), I see it's because of a security mismatch between the server and client.

The requested upgrade is not supported by 'net.tcp://localhost:49627/MyService'. This could be due to mismatched bindings (for example security enabled on the client and not on the server).

Checking the host's diags (again: crush, drive, read logs, enjoy lamentations), I see this is true

Protocol Type application/ssl-tls was sent to a service that does not support that type of upgrade.

"Well, self," I says, "I'll just turn on Message security on the host!" And I do. If you want to know what it looks like, it's an exact copy of the client config. Look up.

Result: Kaboom.

The binding ('NetTcpBinding','http://tempuri.org/') supports streaming which cannot be configured together with message level security. Consider choosing a different transfer mode or choosing the transport level security.

So, my host cannot be both streamed and secured via tokens. Catch-22.

tl;dr: How can I secure a streamed net.tcp WCF endpoint using WIF???

Best Answer

WCF has gotchas in a few areas with streaming (I'm looking at you, MTOM1) due to a fundamental issue in how it fails to perform preauthentication the way most people would think that should work (it only affects subsequent requests for that channel, not the first request) Ok, so this is not exactly your issue but please follow along as I will get to yours at the end. Normally the HTTP challenge works like this:

  1. client hits server anonymously
  2. server says, sorry, 401, I need authentication
  3. client hits server with authentication token
  4. server accepts.

Now, if you ever try to enable MTOM streaming on an WCF endpoint on the server, it will not complain. But, when you configure it on the client proxy (as you should, they must match bindings) it will explode in a fiery death. The reason for this is that the above sequence of events that WCF is trying to prevent is this:

  1. client streams 100MB file to server anonymously in a single POST
  2. server says sorry, 401, I need authentication
  3. client again streams 100MB file to server with an authentication header
  4. server accepts.

Notice that you just sent 200MB to the server when you only needed to send 100MB. Well, this is the problem. The answer is to send the authentication on the first attempt but this is not possible in WCF without writing a custom behaviour. Anyway, I digress.

Your Problem

First up, let me tell you that what you're trying is impossible2. Now, in order for you to stop spinning your wheels, let me tell you why:

It strikes me that you are now wandering in a similar class of problem. If you enable message level security, the client must load the entire stream of data into memory before it can actually close out the message with the usual hash function and xml signature required by ws-security. If it has to read the entire stream to sign the single message (which is not really a message, but it's a single continuous stream) then you can see the problem here. WCF will have to stream it once "locally" to compute the message security, then stream it again to send it to the server. This is clearly a silly thing, so WCF does not permit message level security for streaming data.

So, the simple answer here is that you should send the token either as a parameter to the initial web service, or as a SOAP header and use a custom behaviour to validate it. You cannot use WS-Security to do this. Frankly, this is not just a WCF issue - I cannot see how it could practically work for any other stacks.

Solving the MTOM Problem

This is just for an example how I solved my MTOM streaming issue for basic authentication, so perhaps you could take the guts of this and implement something similar for your issue. The crux of it is that in order to enable your custom message inspector, you have to disable all notion of security on the client proxy (it remains enabled on the server,) apart from transport level (SSL):

this._contentService.Endpoint.Behaviors.Add(
    new BasicAuthenticationBehavior(
        username: this.Settings.HttpUser,
        password: this.Settings.HttpPass));
var binding = (BasicHttpBinding)this._contentService.Endpoint.Binding;
binding.Security.Mode = BasicHttpSecurityMode.Transport; // SSL only            
binding.Security.Transport.ClientCredentialType = 
   HttpClientCredentialType.None; // Do not provide

Note that I have turned off transport security here because I will be providing that myself using a message inspector and custom behaviour:

internal class BasicAuthenticationBehavior : IEndpointBehavior
{
    private readonly string _username;
    private readonly string _password;

    public BasicAuthenticationBehavior(string username, string password)
    {
        this._username = username;
        this._password = password;
    }
    public void AddBindingParameters(ServiceEndpoint endpoint, 
        BindingParameterCollection bindingParameters) { }
    public void ApplyClientBehavior(ServiceEndpoint endpoint,
        ClientRuntime clientRuntime)
    {
        var inspector = new BasicAuthenticationInspector(
            this._username, this._password);
        clientRuntime.MessageInspectors.Add(inspector);
    }
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
        EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceEndpoint endpoint) { }
}

internal class BasicAuthenticationInspector : IClientMessageInspector
{
    private readonly string _username;
    private readonly string _password;

    public BasicAuthenticationInspector(string username, string password)
    {
        this._username = username;
        this._password = password;
    }

    public void AfterReceiveReply(ref Message reply,
        object correlationState) { }

    public object BeforeSendRequest(ref Message request,
        IClientChannel channel)
    {
        // we add the headers manually rather than using credentials 
        // due to proxying issues, and with the 101-continue http verb 
        var authInfo = Convert.ToBase64String(
            Encoding.Default.GetBytes(this._username + ":" + this._password));

        var messageProperty = new HttpRequestMessageProperty();
        messageProperty.Headers.Add("Authorization", "Basic " + authInfo);
        request.Properties[HttpRequestMessageProperty.Name] = messageProperty;

        return null;
    }
}

So, this example is for anyone who is suffering with the MTOM issue, but also as a skeleton for you to implement something similar to authenticate your token generated by the primary WIF-secured token service.

Hope this helps.

(1) Large Data and Streaming

(2) Message Security in WCF (see "disadvantages.")

Related Topic