Click here to Skip to main content
15,868,141 members
Articles / Web Development / IIS

ClickOnce Licensing HTTPHandler

Rate me:
Please Sign up or sign in to vote.
4.63/5 (7 votes)
22 Apr 2011CPOL6 min read 38.5K   651   54   9
A HTTPHandler to allow licence validation for ClickOnce applications

Introduction

I've recently been working on a small application I intended to sell publicly and I was faced with the dilemma of how to deploy it for an individual user. My initial approach was with an MSI but this seemed a bit clumsy, especially around updating, and required me to learn quite a bit about installers that I didn't really care for. ClickOnce seemed like a much simpler alternative so I've recently re-released the application with a ClickOnce deployment. However, one downside to ClickOnce is that once you make it available on the Internet, it's available for anyone who can navigate to the address; there doesn't seem to be any concept of licensing. As I was selling the application, this seemed like a quite major shortcoming of ClickOnce, I wanted to make it at least a little bit challenging for someone to obtain my application without paying for it.

After quite a lot of research and a little bit of development, I've created this HTTP Handler that provides some level of licensing for ClickOnce applications and I thought I'd share it in the hope that it might save someone some time in deploying their application. I should point out that this code isn't intended to be compiled and deployed straight to your production environment as a solution to ClickOnce licensing. Security, particularly web based, is not my strong point and the code I provide is a base implementation only. I made some modifications for my own deployment of this HTTP Handler and you will probably need to as well. At the very least, you should review the code and understand its workings and limitations.

Background

I wanted to require a user to have a valid licence key to download the application and to perform updates, but I also wanted to allow a user to run the application even if they didn't have an Internet connection or the licensing server was down. Some research, particularly a helpful MSDN article on Administering ClickOnce Deployments, suggested the easiest way to do this was to provide a query string on the ClickOnce URL and a HTTP Handler to intercept the request and provide the validation.

The biggest issue with this is that a ClickOnce application has a pre signed manifest which details where the application is installed from and where updates can be retrieved from. The query string with the licence key will be different for every user but the manifest can't be easily updated because that would break the signing.

The 'solution', and I use that term in a loose sense as I'll soon explain, is to use the HTTP Handler to update and resign the manifest before returning it in the response.

The reason this is not a complete solution is that it has some disadvantages:

  1. You need the key used to sign the manifest to be somewhere the HTTP Handler can access it, which may be on a shared web server somewhere. This wasn't a great issue for me because at this stage my application is self signed anyway and I've taken some measures to attempt to protect the key.
  2. The bootstrapper setup.exe generated by Visual Studio for your ClickOnce application will no longer be able to start the ClickOnce installer after the prerequisites are installed as the URL will be wrong. After some failed attempts to generate the bootstrapper dynamically in the HTTP Handler, I've resorted to generating a generic standalone bootstrapper using MSBuild (from the command line) which does not try to launch the ClickOnce installer when it finishes. It means there is no nice transition from the bootstrapper to the ClickOnce installer but I can live with that for now.

I'd already created a simple licensing server to generate and validate licence keys (it's called by my payment provider when someone makes a purchase). I implemented this as a WCF REST(ish) service and, as I'd been reading through Scott Hanselman's article on Async IO, I decided I'd wrap up an Async GET request to the licensing server in a small helper class.

I was hesitant to provide code snippets directly as I'm not really adding anything new here, if you read the linked articles and associated source code that's basically what you're getting but put together with some licence validation and in a configurable 'ready to deploy' type module. However, to broaden the target audience of this article and give a bit more insight, I'll go through the basic process:

  1. Extract the licence key from the query string:
    C#
    string licenceKey = context.Request.QueryString["licenceKey"];
    
    if (string.IsNullOrWhiteSpace(licenceKey))
    {
          logger.Fatal("Request: {0} - 
    	Empty licence and/or application key provided.", requestId);
          context.Response.Redirect(ConfigurationManager.AppSettings
    	["LicensingErrorRedirectUrl"]);
    } 
  2. Validate the key with an Async request to the licensing server. Redirect to a configured page if licensing fails.
    C#
    ValidateLicenceKeyAsync(licenceServerAddress, licenceKey, applicationId)
    .ContinueWith(task =>
    {
            if (string.IsNullOrWhiteSpace(task.Result))
            {
                    logger.Error("Request: {0} - 
    		No response returned from Licensing Server", 
    			requestId, licenceKey);
                    result = false;
            }
    
            result = task.Result == 
    	CreateHexMd5Digest(applicationKey + bool.TrueString);
    })
    .Wait();

    Inside ValidateLicenseKeyAsync is a normal Async HttpWebRequest, it's just wrapped in a Task.

    C#
    validationRequest.BeginGetResponse(iar =>
    {
            try
            {
                    using (HttpWebResponse response = 
    		(HttpWebResponse)validationRequest.EndGetResponse(iar))
                    {
                            using (StreamReader responseStreamReader = 
    			new StreamReader(response.GetResponseStream()))
                            {
                                    string licenceResponse = 
    				responseStreamReader.ReadToEnd();
                                    tcs.SetResult(licenceResponse); //set the result 
    							//of the Task
                            }
                    }
             }
             catch (Exception ex) { tcs.SetException(ex); } //Set the exception 
    						//in the Task
    }, null);
  3. Update and resign the application manifest. This is taken almost directly from the source attached to Brian Noyes' ClickOnce article on MSDN. However X509KeyStorageFlags.MachineKeySet is required to ensure the key is loaded from the local file rather than attempting to load it from the user store.
    C#
    //Generate the new ClickOnce deployment URL with licence key
    string providerUrl = string.Format("{0}?licenceKey={1}",
                                       context.Request.Url.GetLeftPart
    				(UriPartial.Path),
                                       licenceKey)
    manifest.DeploymentUrl = providerUrl;
    //You'll need to give the AppPool user write permission 
    //to the temp manifest directory
    ManifestWriter.WriteManifest(manifest, manifestPathOut);
    X509Certificate2 cert = new X509Certificate2
    	(certPath, certFilePassword, X509KeyStorageFlags.MachineKeySet);
    SecurityUtilities.SignFile(cert, null, manifestPathOut); 
  4. Return the manifest in the response:
    C#
    context.Response.WriteFile(tempManifestPath);
    context.Response.ContentType = "application/x-ms-application"; 

Using the Code

I've provided the source code for the HTTP Handler and it should compile and run as it is. It contains a very simple licensing validation that is an example only and I recommend you replace it with something more substantial. You may also wish to encrypt the licence keys and application Ids that are passed around as query strings.

I have also provided an example web.config file that contains the configuration values used by the HTTP Handler. I have an MVC 3 website for my application and this is also where I host the ClickOnce files so all the configuration values for the HTTP Handler are included in the web sites web.config file.

App Settings

  • ValidateLicence - True to enable licence validation, false to turn it off.
  • LicenceServerAddress - The address of the licensing server including the query string for an application id and licence key. This will be used in a string.Format() to add the query string parameter values.
  • ApplicationId - The unique Id of the application that the licence will be validated for, this assumes the licensing server holds licenses for more than 1 application and may not be required in your situation.
  • ApplicationKey - I've provided a very basic licence key validation that assumes the server returns an MD5 digest of the combination of the ApplicationKey configured here and a value indicating if the licence key is valid. As such, this key would be shared with the licensing server.
  • CertificatePath - The path to the certificate used to sign the manifest.
  • TempManifestDirectory - The directory and filename where the licence key specific manifests will be generated and saved. The {0} will be replaced with a GUID that should be unique to each request. Manifests in this directory can be removed or the HTTP Handler could be updated to reuse them for requests with the same licence key.
  • LicensingErrorRedirectUrl - If there is an error validating the licence OR it fails validation, then the user will be redirected to this URL.
XML
<appSettings>
  <add key="ValidateLicence" value="true"/>
  <add key="LicensingServerAddress"
  value="HTTP://WWW.EXAMPLE.COM/VALIDATE?APPID={0}&LICENCE={1}" />
  <add key="ApplicationId" value="APPLICATION_ID" />
  <add key="ApplicationKey" value="PRIVATE_APPLICATION_KEY" />
  <add key="CertificatePath" value="~/PRIVATE_CERTIFICATE.pfx"/>
  <add key="TempManifestDirectory" value="~/TEMP/APPLICATION{0}.application"/>
  <add key="LicensingErrorRedirectUrl" value="~/LICENCE_FAILED"/>
</appSettings>

Below is the registration of the handler as well as a handler to block the .pfx file from being served and hidden segment for the directory in which the .pfx file is stored.

XML
<system.webserver>
    <handlers>
      <add name="pfx-blocking" path="*.pfx" verb="*" 
	type="System.Web.HttpForbiddenHandler" 
	resourcetype="File" requireaccess="Script">
      <add name="ClickOnceLicensing" path="*.application" verb="*" 
	type="ClickOnceLicensingModule.ClickOnceLicensing, ClickOnceLicensingModule">
    </add></add></handlers>

    <security>
      <requestfiltering>
        <hiddensegments>
          <add segment="PRIVATE_KEY_DIRECTORY">
        </add></hiddensegments>
      </requestfiltering>
    </security>
  </system.webserver> 

History

  • 2nd April, 2011: Initial post
  • 20th April, 2011: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralInteresting article, I took a different approach. Pin
John Orendt3-Mar-13 23:30
John Orendt3-Mar-13 23:30 
GeneralMy vote of 5 Pin
craig61a3-Feb-12 7:59
craig61a3-Feb-12 7:59 
GeneralAsync HttpWebRequest Pin
KevinAG20-Apr-11 16:24
KevinAG20-Apr-11 16:24 
GeneralRe: Async HttpWebRequest Pin
Member 518889820-Apr-11 18:04
Member 518889820-Apr-11 18:04 
GeneralRe: Async HttpWebRequest Pin
Member 518889821-Apr-11 1:07
Member 518889821-Apr-11 1:07 
GeneralThat's good Pin
Member 76670612-Apr-11 6:07
Member 76670612-Apr-11 6:07 
Generalinteresting Pin
Not Active2-Apr-11 3:06
mentorNot Active2-Apr-11 3:06 
GeneralRe: interesting Pin
Member 51888982-Apr-11 4:27
Member 51888982-Apr-11 4:27 
GeneralRe: interesting Pin
Not Active2-Apr-11 4:33
mentorNot Active2-Apr-11 4:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.