One Service + Two Client Platforms = Misery

Friday, May 08, 2009 / Posted by Luke Puplett /

Aim: I want to be able to transmit a username and password hash and see it easily from within the service.

My idea of Hell is WCF. I liked .NET Remoting and I got on with Web Services, but the unified Windows Communication Foundation is a quagmire of configuration and complexity, not least because its half-merged with IIS, which is sort of merged but not merged with ASP.NET.

Today, I am tackling the issue of using the same WCF Service that my Silverlight client uses, for a fat desktop client.

Making a Meal of WCF and Cookies

The scenario to the uninitiated might look straight-forward, after all I’m connecting two Microsoft .NET-based products to a Microsoft .NET-based SOAP service, all running in the same VS solution on one box.

The problem is that Silverlight runs in the browser and has an authentication scheme which uses its host browser’s HTTP stack and cookies, while the other client, despite being fat, has never even seen a cookie.

To be clear, my WCF services end uses Microsoft’s AuthenticationService to provide Forms authentication behaviour over the HTTP connection between SL client and service, which basically does the cookie creation and response so I don’t have to. This works great but I what about my non-Silverlight clients?

While Jonas Folleso has one solution to the problem, by detaching the authentication cookie and sticking it back on to all outgoing headers, I wanted to take the ‘easier’ route: exposing another service end-point that supports some kind of authentication.

I want to do this because I want my client to sign-in using the SSL secured AuthenticationService, set-up a session with some shared secret key and then in all subsequent requests to my other services, send over a hash of something using the shared secret.

I’m actually writing this as I work on it, so as these words are being typed, I don’t know what to expect.

Step One: Add a new EndPoint and connect to it

I add a new EndPoint to the web.config of my IIS service host, using the same basic binding as the working service, just for simplicity and testing.

<endpoint address="/vpr"
binding="basicHttpBinding"
contract="IUserService"
/>

Lesson - While doing this, I learn that the <baseAddresses> section is pointless on IIS because its IIS and the base address will be wherever the .svc files are.

I copied the chunk of XML that defines the mex endpoint, and like mex and according to MSDN, my service endpoint address will be relative to the base URL which is the svc file, I presume.


Adding a Service Reference to any of the URLs above fails with error saying:


The request failed with HTTP status 400: Bad Request. Metadata contains a reference that cannot be resolved: 'blah'.

Lesson – You only need to point the Add Reference box to the .svc file and the other endpoints are read from the WSDL (you can see this because the client app.config will fill with the server endpoints).

mex is an exception to the rule and can be referenced by putting /mex after .svc, also the discover option doesn’t see the endpoint as being a different service.

This is all how it should be and I think endpoint addresses are actually just ID strings which help WCF route messages into the right worker queue for processing on the server but are structured as URLs to confuse you.

Step Two: Choose a Binding

The aim is simply to choose a binding which puts the plumbing in place for me to transmit credentials. So first up, I try just setting ClientCredentials.UserName to something useful and checking it on the server via ServiceSecurityContext.Current, but get a null ref exception.

I also try disabling anonymous logon from the IIS console in case this is causing my username to be lost, but this causes everything to break – I can’t even connect to get the service description.

Lesson – Ignore the bit about disabling anonymous access.

I persist with the trusty basicHttpBinding because MSDN says:

By default, the BasicHttpBinding class does not provide security. This binding is designed for interoperability with Web service providers that do not implement security. However, you can switch on security by setting the Mode property to any value except None. To enable transport security, set the property to Transport.

I do as it says but I get this error upon updating my Service Reference:

Could not find a base address that matches scheme https for the endpoint with binding BasicHttpBinding. Registered base address schemes are [http].

The problem is that I don’t want to use HTTPS. Time to try another tack: Message Security, which MSDN tells me

means that every message includes the necessary headers and data to keep the message secure. Because the composition of the headers varies, you can include any number of credentials. This becomes a factor if you are interoperating with other services that demand a specific credential type that a transport mechanism can't supply, or if the message must be used with more than one service, where each service demands a different credential type.

Here goes nothing.

        <binding name="vpr">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>

I refresh my Service References and receive:

BasicHttp binding requires that BasicHttpBinding.Security.Message.ClientCredentialType be equivalent to the BasicHttpMessageCredentialType.Certificate credential type for secure messages. Select Transport or TransportWithMessageCredential security for UserName credentials.

Right then. TransportWithMessageCredential it is, or at least it would be but that of course leads me right back to:

Could not find a base address that matches scheme https for the endpoint with binding BasicHttpBinding. Registered base address schemes are [http].

An MSDN article on using the ASP.NET Membership Provider reckons I can use WSHttpBinding set to Message Security to transmit a user name, but goes on to talk about adding service behaviours for the membership provider. I will try most of it (as I don’t want to use the ASP.NET MemProv). I put

    <serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"/>
</serviceCredentials>

..in the behaviours element with this in the binding element:

    <security mode="Message">
<message clientCredentialType="UserName"/>
</security>

But this gives this error when adding/updating the Service Reference:


UserNamePasswordValidationMode.Custom requires a CustomUserNamePasswordValidator. Specify the CustomUserNamePasswordValidator property.


I may be on to something... searching microsoft.com reveals an article entitled User Name Password Validator.

So I’ve written a test class called CookielessCredentialValidator which extends System.IdentityModel.Selectors.UserNamePasswordValidator and just throws a NotImplementedException on the only overridable method is has, Validate() and changed my web.config:

<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="App.Web.CookielessCredentialValidator"/>

Upon updating my service reference I get:


Could not load type ‘App.Web.CookielessCredentialValidator’ from assembly ‘System.ServiceModel, Version=3.0.0.0, Cul... etc.

Looking back at the MSDN article I realised that I need to format the type name differently, prefixing my derived class with the name of my service class and maybe the word service after it? Not really clear and the article on customUserNamePasswordValidator provides no help:

Looking further into this, it seems that I must create an X509 Certificate.

Lesson – You can’t send credentials without HTTPS or some other encryption method in the .NET Framework, even if you do have you own method for encrypting the data you wish to put in the username and password properties.

Step Three: Go back to Jonas Folleso’s Blog and do what he did

...and pray that Microsoft changes this nannying part of the framework before I get customers trying to use my service from a platform that can’t fiddle around with cookies.

The End.

P.S. Sorry about the extra space around blockquotes, the Blogger system seems to want to add breaks each and every time I place a carriage return in pure HTML - bizarre and annoying in equal measure.

Labels: , , , ,

0 comments:

Post a Comment