ClickOnce and cross platform integration

by Administrator 25. September 2007 17:57

ClickOnce is so close to greatness, as I’ve said before.  Yes, I know the “official word” is that this is not really meant for software ISVs deploying software to end users but it seems with a few extra things it could indeed be very good for this.  With some features in the Orcas Beta 2, I think ClickOnce is getting much closer to providing the plumbing I’ve been wanting in some cases and home rolling in others.  A new product I’m working on has some Architectural Desires.  I call them “desires” because we’ve been selling desktop applications without these architectural features for a while but I really want to meet some Service Level Requirements that will make my life easier, the customer’s life easier, and our support staff’s life easier.

Environment

·         I have an existing web system that users sign into.  Credentials are stored in an app-specific schema in a MySQL database.  Users of the current desktop application must be in this MySQL database and authenticate over web services.  Users of the new system increasingly do not need access to the web based system.  Putting their credentials in MySQL is polluting this system.

·         We have an OpenLDAP server we are slowing moving credential stores to.

·         We have a Windows2003 server that’s not doing much right now

·         We have a client application that ships with specialized hardware and an install disk.

·         Upgrades go out to customers via installing a new MSI

Requirements/Desires

1.       My software requires some specialized hardware.  I’d like to ship this without install disks.

2.       I’d like the desktop application to be able to have the Role information and authenticate against our existing credential stores.  I don’t want users to be able to run this within a very short time period if we turn them off.

3.       I’d like to stop polluting the existing web system credential store with users that should not ever be able to log into this system, however SOME users of the web based system will also need to be able to authenticate using the client application.

4.       I’d like to be able to keep random people from stealing software.

5.       I’d like to be able to keep random people from installing my software.

 

Solution in Code and UML

Let’s take the big red pill and see where it leads us.

ClickOnce - Part 1

I start with .NET 3.5 Beta 2, VS 2008 Beta 2, and my current environment.  Setting up ClickOnce deployment is easy enough, I set the application to check for updates before it runs, and verify that this works.  Oddly enough with Beta 2, 100% of my testers who have tried to click “Install” rather than installing .NET 3.5 and choosing “launch” have had the install .exe lock up.  Hopefully this gets fixed.

Client Application Services – Server Side

There is a wonderful bit of plumbing available now Called Client Application Services.  In order to use this you need an ASP.Net web site.  You need to add a reference to System.Web.Extensions, and configure your web.config such that the Role and Membership services are enabled.  With .NET 3.5 this is done like so:

      <system.web.extensions>

            <scripting>

                  <webServices>

                        <authenticationService enabled="true" requireSSL="false"/>                   

                        <roleService enabled="true"/>

Now, the Role and Membership providers I have configured are my LDAP based home rolled code.  Discussing these is outside the scope of this article; suffice to say they are straight up Provider implementations.  I will need to set up my Forms authentication a certain way to meet my single sign on goals:

            <authentication mode="Forms">

                  <forms domain=".office.carspot"

                           enableCrossAppRedirects="true"

                           cookieless="UseCookies"

                           slidingExpiration="false"

                           defaultUrl="Cookies.aspx"

                           path="/"

                           protection="Encryption"

                           timeout="1440"

                           name="FooBar"/>

            </authentication>

For testing I am allowing the clients to sign on once for a day.  The domain attribute is important, as the cookie created by my web site will now be passed to anysite.office.carspot.  There is one more thing to configure here.  At this point I intended that I would manually provide the Symmetric Key to Trusted Applications written in PERL or Python etc. to decrypt the data in the cookie.  I therefore manually created some keys for a MachineKey element.  This will still be needed when I cluster this solution.

      <system.web>

            <machineKey validationKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            decryptionKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            validation="SHA1" decryption="AES" />

Client Application Services – Client Side

With the server side set up, I return to my ClickOnce application.  There will be some work to do in the Properties of my app and some work to do in code.  The first step is to enable Client Application Services.

Only the root directory of the ASP.Net application is required.  This also requires references to System.Web and System.Web.Extensions in my Client application.  This is the first hole I’ve seen thus far in my argument that a Client-only .NET install should be available.  The advanced tab bears some discussion now and also a little later:

In my case I want to make sure they are authenticating to the server at regular intervals.  The Use custom connection string setting is the key to a later problem.  Interested readers may want to use reflector on System.Web.ClientServices.Providers.ClientData,  System.Web.ClientServices.Providers.ClientDataManager, and System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, as well as exploring the new System.IO.IsolatedStorage namespace, unless using Reflector on system assemblies is against some rule, in which case you should not and I never have done so.

Now I need to write some code and see if I can log in.  Note that back in the Services tab I included a Class and Assembly name for a credential provider type.  This tells the plumbing what method to invoke in order to request credentials from the users.  The class must implement IClientFormsAuthenticationCredentialsProvider, in my case this is a login window written in WPF.  Insofar as my research has gone so far, authenticating members and checking roles must be done explicitly.  The code to do so is simple:

bool valid = System.Web.Security.Membership.ValidateUser("", "");

This will cause the plumbing to invoke my Login window and give me an answer as to whether authentication was successful, and since Role management is enabled I should be able to get at my roles as well.  Now I’m going to set up a proxy and try to log in.  Here’s what Fiddler shows me:

 

You can see the AJAX/JSON requests going back and forth, and I’m showing the headers of the response in order to show you the persistant cookie.  The actual body of the response is just {“d”, true}.  Now I’m going to try to get this to authenticate against a Web service written in another language on a different server.

This Cookie is not for you

My thought was to use cookies with a common Domain in order to authenticate against my ASP.NET site via Client Services but have the information available in a usable form to other non-.NET systems.  This turned out to be a pipe dream at first.

·         Authenticating via Login.aspx returns a cookie as expected.  Cookies are turned off for HttpWebRequest by default.  Nothing I could find would get Windows/.NET to nicely populate cookies on my HttpWebRequest once I gave it a CookieContainer.  This is probably desired behavior.

·         Authenticating via Client Services does not create a cookie in the same way IE does anyway (more on this in a moment).

·         Any IE specific hacks I do are likely to be broken in Firefox, and supposedly ClickOnce will work with Firefox later this year.

I started by experimenting with the Use Custom Connection String setting, but realized this may be problematic in ClickOnce since the actual Application directory is not a clean or dependable Path.  When facing a dilemma like this I pull out my handy File System Watcher program and go to work. 

While logging in, this “User_damonpayne.clientdata” file created and changed looks promising, and it turns out to be.  I can already tell by calling Thread.CurrentPrincipal.Identity.Name that my server-side user name “damonpayne”, is indeed available on the Client.  Opening up this .clientdata file reveals the following:

<?xml version="1.0" encoding="utf-8"?>

<ClientData>

      <LastLoggedInUserName></LastLoggedInUserName>

      <LastLoggedInDateUtc>1C6E0B92F1DBE7E</LastLoggedInDateUtc>

      <PasswordHash></PasswordHash>

      <PasswordSalt></PasswordSalt>

      <Roles>

            <item>Default</item>

            <item>Technology</item>

            <item>ScrumMaster</item>

      </Roles>

      <RolesCachedDateUtc>1C7FF8AA84CDD3E</RolesCachedDateUtc>

      <SettingsNames></SettingsNames>

      <SettingsStoredAs></SettingsStoredAs>

      <SettingsValues></SettingsValues>

      <SettingsNeedReset>0</SettingsNeedReset>

      <SettingsCacheIsMoreFresh>0</SettingsCacheIsMoreFresh>

      <CookieNames>

            <item>09a630885b744552b25cf95da4f7e20f</item>

      </CookieNames>

      <CookieValues>

      <item>SoloAuth=9816CAC289A2ADBE7E88A0498E71733E0C480CAD631CB57460BF8AB9822747A43FE1ABE6F0D20DCAA95D8339FE8EC866C878A10A1A2BB188AE42B50DCB4C9862</item>

      </CookieValues>

</ClientData>

Those are indeed my roles, and that does look like my server cookie value.  How convenient.  Of course from the Client I can simply call System.Web.Security.Roles.GetRolesForUser() to get my roles, but remember that I sought to pass a token to someone else that would vouch for who I am.  I will now spare you some of the details of my research and skip to the punch line.  By converting my Hex string from <machinekey/> back to a byte array I was hoping to privately share this Symmetric Algorithm key with trusted systems and allow them to decrypt the cookie value.  Well, this didn’t work, and switching algorithms from AES to 3DES didn’t work, and changing the key sizes didn’t work.  Digging through FormsAuthentication through reflector I found the 1st issue: the data is not plain text but rather a binary serialized object.  Ok, so one could use BitConverter to pull out the desired values.  The real sticking point is that FormsAuthentication, when it creates a Symmetric Algorithm of the type specified by your <machineKey/> setting, creates a random Initialization Vector that is not available to you.  How this voodoo works in a clustered environment I don’t know, but this behavior is somewhat disappointing.   Community Server, for example, allows you to put the IV in the web.config as well since this is a very necessary part of the cryptographic process.  My hopes of sharing a symmetric key with a trusted application were killed, and I spent quite a bit of time on Google and USENET looking for a solution.  This was one of those situations where the obvious answer eluded me for a while because I had been too close to this problem for too many hours. 

I can still write code to dig out the cookie value:

        /// <summary>

        /// Assuming local storage, get the client data cookie for this application

        /// </summary>

        /// <returns></returns>

        public static string GetClientDataCookie()

        {

            string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

            string ver = Assembly.GetCallingAssembly().GetName().Version.ToString();

            appData = Path.Combine(appData, @"CarSpot\CarSpot.Solo\" + ver);

 

            string userName = Thread.CurrentPrincipal.Identity.Name;

 

            string userFile = "User_" + userName + ".clientdata";

            string path = Path.Combine(appData, userFile);

            XmlDocument doc = new XmlDocument();

            doc.Load(path);

            string token = doc["ClientData"]["CookieValues"]["item"].InnerText;

            token = token.Substring(token.IndexOf("=") + 1);

            return token;

        }

… and I still have a public facing Web server that magically (in memory?) has access to the Initialization Vector:

        [WebMethod(Description="Attempt to decrypt a Forms auth ticket issued from this server farm")]

        [SoapDocumentMethod(ParameterStyle=SoapParameterStyle.Bare)]

        public ValidateClientTicketResponse ValidateClientTicket(string ticketString)

        {

            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(ticketString);

            ValidateClientTicketResponse response = new ValidateClientTicketResponse();

            response.Expiration = ticket.Expiration;

            response.Name = ticket.Name;

            response.UserData = ticket.UserData;

            return response;

        }

… and of course, for trusted systems, I can secure this Endpoint however I please.  I can pass my Ticket from the client to anyone, who can call this service to determine if I am who I say I am and if this is a valid, unexpired ticket.

 

Solution Diagram

So, this is my environment with the solution in place.  Users who only need my Client Application and its accompanying Web Application are created in LDAP.  Users of the special Perl web site that also need to have access to my client are pushed to LDAP via a replication scheme.    My client application can pass it’s authentication ticket value as a SOAP header or an in-band argument when communicating with other trusted endpoints. 

Since I worked through this step by step, there may be a slightly cleaner way to do some of this.  I have been meaning to see if WS-Federation could be used, if only as a more standard API for the web service.

Final Thoughts

Of my original requirements, I have met most of them.  I can keep random people from stealing my code by deploying Obfuscated assemblies via ClickOnce.  I can keep people from running my application using the authentication, and with this method many customers will only have a single password to remember for all their CarSpot products.  I did not write about it here but the ASP.Net Profile information is available via this method as well.  This opens up the door for many unexpected niceness for our users; by implementing a new custom Profile provider, someone might log into one of the Perl web sites and change their display preferences, to discover them magically reflected in their client application as well. 

As for keeping random people from ever even installing my software to begin with, that will have to come later or Obfuscation will have to do.  It would be ideal to protect the .application file for my program so that unwanted visitors cannot even download the manifest in the first place.  Are you listening, Microsoft?  ClickOnce should be able to respond to an authorization challenge or SSL certificate warning in some fashion.   In our tests, Firefox still does not work for launching a ClickOnce application. Are you listening Microsoft?

Tags:

Comments (4) -

Harold
Harold
1/16/2008 9:34:36 AM #

I appreciate if you could help in this. You wrote "The class must implement IClientFormsAuthenticationCredentialsProvider,
in my case this is a login window written in WPF.", how did you implement the GetCredentials method since it is not a form, and the ShowDialog() is not available ?

I appreciate your help.

Reply

ae
ae
10/30/2008 9:10:59 AM #

Hi mister,

any full sample code (for download) about it, please ??

Thanks.

Reply

Paritosh
Paritosh
4/23/2009 3:48:52 AM #

<blockquote cite="Damon">
As for keeping random people from ever even installing my software to begin with, that will have to come later or Obfuscation will have to do.  It would be ideal to protect the .application file for my program so that unwanted visitors cannot even download the manifest in the first place.  Are you listening, Microsoft?  ClickOnce should be able to respond to an authorization challenge or SSL certificate warning in some fashion.
</blockquote>

Did you manage to achieve this ?

Reply

Damon Payne
Damon Payne
4/24/2009 8:48:56 PM #

I did somewhat achieve this.  I ended up creating an Http handler for the application manifest file so that instead of myserver.com/appmanifestfile it used myserver.com/appmanifestfile?productKey=key.  This involved some trickery such as need to alter the deployment URL inside the manifest and re-sign the manifest.  This at least keeps people from learning the assembly names and grabbing everything easily.

Reply

Pingbacks and trackbacks (2)+

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading


About the author

Damon Payne is a Microsoft MVP specializing in Smart Client solution architecture. 

INETA Community Speakers Program

Month List

Page List

flickr photostream