Introduction

If you have tried to secure access to an online ClickOnce deployed application from the Internet before, then you have probably run into the fact that ClickOnce doesn't support any security mechanism other than Windows Integrated, and that too, only over an intranet.

The development shop that we are working at provides an Enterprise-level application that needs to be deployed via ClickOnce (more specifically, it is a .NET 3.0 WPF XBAP), and for some of our clients, it is unacceptable to say, "Anyone on the internet will be able to download the client portion of our application, but our application security will prevent them connecting that client to the backend service". In short, we need a way to throw up some credentials and prevent the app from even being deployed to unauthorized clients.

Background

ClickOnce is a very cool piece of deployment technology, but it often seems like it hasn't received much love and affection from its daddy (read: Microsoft). Although, this may be a bit unfair, as we suspect some of its deficiencies are actually related to how ClickOnce needs to interface with Internet Explorer and FireFox.

First, we need to talk about why most traditional methods for securing websites don't work for ClickOnce. A lot of the way that ClickOnce operates is shrouded in mystery and undocumented, but this is what we think we have been able to glean about the sequence of events. This is specific to ClickOnce XBAPS, but should apply to .NET 2.0/3.0 online applications as well.

Let's assume we have Forms authentication turned on and set up to protect all the files in the deployment.

Note: This article, is not a primer on how to use/configure ClickOnce; if you are unfamiliar with the technical details of these, we would suggest that you first read as much of the MSDN documentation that you can find on these subjects and visit the ClickOnce MSDN forums.

You request the deployment manifest from Internet Explorer (and Firefox in .NET 3.5, but let's not even go there right now).

IE goes out to the web server and tries to request the .xbap.

IIS sends back a redirect to the Forms authentication login page.

We enter our login credentials, and hit Submit.

Our login credentials flow back through IIS/ASP.NET, we are authenticated, and are sent back the .xbap.

OK, so now, IE has the .xbap, so it will pass that over to ClickOnce.

ClickOnce will attempt to re-download the .xbap (not sure why it does this, maybe IE has no facility to pass the file contents directly, or maybe ClickOnce just needs to re-download it again for security reasons).

Here's where things start to break down, ClickOnce does not have the authentication cookie that IE set up. So, this request will fail.

Let's say, ClickOnce has somehow been able to retrieve the .xbap. The next request for a file (probably the application manifest) will not have any security context about how to authenticate against IIS/ASP.NET, and as a result, the request will fail.

Microsoft's response to this is that you should keep the client portion (the part that gets downloaded) of your app as thin as possible, and just use application security to defend data, etc. But, this simply won't work if you:

Have software that is deployed by your clients, and they don't want unauthorized users to be able to download the client portion of the program.

Have valuable intellectual property in the client portion of the application that you don't want to bother obfuscating through some other means, or just plain exposing to the Internet.

The Solution

We would really just like to be able to use Forms Authentication to defend the application from being downloaded by unauthorized users, but how can we make this work?

We have one means of communicating information between the initial request made by IE and the subsequent requests that are made by ClickOnce, and that is through the manifests we deploy.

This specifies the path to the application manifest (which will, in turn, contain information about how the rest of the files are to be deployed). The application manifest contains relative paths to the individual files in the application. (Note: Microsoft documentation says that these can be absolute paths, rather than relative, but this isn't true. We tried it out, as it would have made things significantly easier, but alas, the documentation seems to be conflicting.) This path, in the .xbap, serves as a base for all the relative paths in the application manifest.

So, how can we take advantage of this? Well, ASP.NET has a magical little piece of functionality called cookieless forms authentication. And, it looks like this:

http://SOME_DOMAIN/(F(GARBAGE))/SOME_FILE

Where GARBAGE is actually an encrypted forms authentication cookie. ASP.NET has this functionality because there are some devices that don't support cookies being transported in the HTTP session, or don't support a way to store these cookies locally.

A forms auth cookie can also be sent like this:

http://SOME_DOMAIN/SOME_FILE?AuthCookieName=ENCRYPTED_COOKIE_DATA

But, we're guessing some devices may not even support holding on to query strings. So, what the cookieless support does, is to turn the auth ticket into a fake directory in the URL path. This is perfect for us, because we actually cannot use a query string, because as soon as we do, it's considered an absolute path by ClickOnce, and rejected.

where (F(GARBAGE)) is a validly constructed cookieless auth token for a Fforms authentication ticket that has not expired, then ASP.NET should be presented with all the information it needs to do Forms authentication on the request for the application manifest, and each subsequent file request. Remember that the codebase on the application manifest, SOME_PATH/(F(GARBAGE)), will be used as the URL base for the subsequent relative paths.

So, the plan is to write an IHttpHandler to service requests for the .xbap, and instead of serving up the requested .xbap, distill information about the current auth session, pump it into the .xbap, re-sign the .xbap, and then push that down to the client. Hooray!

There is, of course, just one more snarl in the fabric. When ClickOnce makes its first request for the .xbap, this request needs to have the authentication information. Mercifully, it seems ClickOnce uses exactly the same URL to request the .xbap as was input in IE, so, as long as we make sure IE includes a query string with the auth ticket, we are golden. And again, ASP.NET helps us here, since cookieless forms authentication can have the cookie encrypted in the query string, and also has this cookieless fake directory thing.

Using the Code

HttpHandler for Cookieless Authentication

And, here's the main function for the handler. This handler will basically only be attached for the .xbap. It calls methods that are going to alter and re-sign the .xbap, and then push that down to the client.

'''<spanclass="code-SummaryComment"><summary></span>''' Entry point for the handler, here we determine if we are dealing with the xbap,
''' and modify its values and resign.
'''<spanclass="code-SummaryComment"></summary></span>'''<spanclass="code-SummaryComment"><paramname="context"></param></span>'''<spanclass="code-SummaryComment"><remarks></remarks></span>PublicSub ProcessRequest(ByVal context As System.Web.HttpContext) :
Implements System.Web.IHttpHandler.ProcessRequest
Dim _path AsString = context.Server.MapPath(context.Request.Path).ToLower
If IO.File.Exists(_path) ThenIf _path.EndsWith(".xbap") Then'Set the correct mime type for xbap
context.Response.ContentType = "application/x-ms-xbap"'Update the xbap with the cookieless authentication 'session informationDim _file AsString = GenerateXbap(context, _path)
'Write the updated file to the response
context.Response.WriteFile(_file)
context.Response.Flush()
'Clean up the temporary file that has been generated.
Directory.Delete(Path.GetDirectoryName(_file), True)
EndIfElseThrowNew HttpException(404, "File not found")
EndIfEndSub

GenerateXbap is, basically, going to create a temporary copy of the existing .xbap, and then modify the path, as described in the previous section, and re-sign. The real work is done here:

A keen observer would notice the X(1) part. We'll ignore this for now, and focus on only creating the Forms Authentication Ticket Faux Directory. Also note, that we are grabbing the forms auth cookie that ASP.NET has been kind enough to already set up for us, due to the user logging in to login.aspx.

You can delve into the attached code for how the manifest re-signing is done here, as it has been discussed elsewhere, and it isn't really related to what we are talking about here.

Now, we need to register the manifests and the deployment files with ASP.NET so that Forms authentication will work for them.

In IIS 7, we do this (note: we are using the classic pipeline in this example):

In IIS 6.0, we do this through the Internet Information Services manager. See here.

Next, we need to attach our handler for .xbap, and attach the StaticFileHandler for the regular deployment files, as we would like them to be downloaded as normal. We only have them registered with ASP.NET in order for the Forms authentication to work.

At this point, everything would be configured for the XBAP, and all the referenced files, to be protected by ASP.NET. Hooray! Of course, another problem comes up.

If the user requests the XBAP with cookieless forms auth, they will get an unique URL to the XBAP containing the faux directory. Now, although the XBAP we are sending back will have the same application identity, ClickOnce will see this unique URL as an identifier of a new install. (Note: We think this may have changed, as we saw a different behavior a few months ago, but it is what it is.) If you didn't mind that a user will have to install your application every time they visited the deployment site, this wouldn't be a problem. This would/should also occur for regular ClickOnce applications, as the deployment provider would be different every time. This is where the X(1) information that we embedded in the application manfiest codebase comes into play.

X(1) to the Rescue

X(1) basically tells ASP.NET, if it is in AutoDetect mode for Forms authentication cookies, that our "Browser" does not support cookies. So, our trick here is to have IE authenticate with a regular cookie, and then switch to using cookieless embedded directories for ClickOnce by marking the codebase (and all subsequent file requests) with X(1). Sneaky!

So, we can make sure that the page we redirect to will have the query string, and thus the first request that ClickOnce makes of the .xbap will be authenticated. The codebase for the application manifest has the embedded forms auth ticket directory, and our XBAP is fully secured with Forms authentication. Hooray!

Points of Interest

The key ideas here are that we can emulate the deployment manifest to pass information between IE and the ClickOnce engine, and that ASP.NET cookieless sessions allow us to support an authenticated session on a "Browser" that does not have the same capabilities as IE. The next thing we want to hit, is what if the client goes further and wants to put an authenticating reverse proxy in front of our ClickOnce deployed app. What mechanisms do we have to have ClickOnce provide security information to, say, an ISA server?

Microsoft said it couldn't be done (or at least was unsupported), but we think we have cajoled ClickOnce (with the help of the brilliant ASP.NET) to support Forms authentication, thus improving the value of ClickOnce hundreds-fold (at least for us).

Thanks

We would like to thank some of the other intrepid adventurers in ClickOnce, as reading their blogs, etc., helped us to figure out enough of what ClickOnce was doing, to implement this solution:

I'm experiencing the exact same problem. Is it caused by some kind of ClickOnce update? The solution looks wonderful, besides this error.

Took the following steps:
1) Setup a project in C# / VS2010 with all steps described in this tutorial.
2) HttpHandler rewrites the .application file, which references now the 'protected' path with the auth ticket (cookie)value.
3) Download starts and ClickOnce opens its installer.
4) Getting the error about different security zones.

Is there anyone around here who has a solution? I doubt if it has anything to do with the security settings, I read on another forum it could be caused by a corrupted file (due to the HttpHandler rewriting?).

This is just what I needed, thank you . I just have one little issue, when I add references to linq and Enterprise Library on the httphandler project, for some reason the site crashes down and I'm no longer able to run the xbap application.

i am using "Forms" authentication mode in my project(built using .Net 2005).

i am using ChartFX-7 to generate Charts, the chart image gets created in Temporary Internet Files.

Now, problem is that when i am using Forms authentication the chart image is not genrated, but when i change authentication mode to None the chart image gets generated. so, i am feeling like Forms authentication mode is restricting the access to the Temporary Internet Files.

Usually we are publish our appliation through Click once technology. Previiously we having the Click onces server in Windows 2003.
Recently we are planning to move that Click once technology in Windows 2008. I create a folder in that server and try to configure
that folder in IIS 7.0(Virtual directory). That time its giving some error. The error is
"The server is configured to use pass-through authentication with a built-in account to access the specified physical path. However, IIS Manager cannot verify whether the built-in account has access. Make sure that the application pool identity has Read access to the physical path. If this server is joined to a domain, and the application pool identity is NetworkService or LocalSystem, verify that <domain>\<computer_name>$ has Read access to the physical path. Then test these settings again."
But i click ok button and trying to browse that folder in IE, it works. So i try to publish my application in that folder, its giving error in my system, the error is "Failed to connect to 'http://srvs056/ClickOnce/test/' with the following error: Unable to create the
Web 'http://srvs056/ClickOnce/test/'. Server error: You cannot convert a virtual directory into a subweb remotely. Ask your web server administrator or ISP to do this for you.", i dont know what do to for this. If you know this issue please provide solution for me

I have my application developed in VB .Net in Visual Studio 2005. My application has a form, and the data from the form is exported to Excel 2007 and I am running macros to perform calculations on the excel spreadsheet. The application as such works fine, but when I publish my application, the publish succeeds and the installation fails giving the error message that - Application Validation didnot succeed. Unable to Continue.. Not able to find the exact cause. Any help would be greatly appreciated.

ERROR SUMMARY
Below is a summary of the errors, details of these errors are listed later in the log.
* Activation of C:\Temp\Poisson_New\Poisson_New.application resulted in exception. Following failure messages were detected:
+ Strong name signature not valid for this assembly stdole.dll.

I get a System.Runtime.InteropServices.COMException when trying to open up your Visual Studio 2008 solution file. I am also using Visual Studio 2008. My Version is 9.0.21022.8 RTM, what is your version? You can get it from going to Help -> About Microsoft Visual Studio. Also what development settings are you using? I was using a C# development environment but I switched to VB and General Environmental settings and still had no luck with opening your project (Tools -> Import and Export Settings -> Reset All Settings, go through the wizard but don't actually select another environment, the one that is selected when it loads is the environmental settings you have).

Thank you for posting this solution by the way. I am sure once I get it working it will be exactly the work around I was looking for.

Hi,
I provide a staff extranet, for which I use a single sign-on process managed through forms authentication and our own custom SQL server user database.

I based my solution on the Wrox book "Professional ASP.NET Security", essentially using a common machine key setting in each applications web.config file.

I recently (end of 2007) started getting into building ClickOnce apps and needed to protect the installation of these in the same manner as our web-apps.

My approach was to use the same system used in the single sign-on, so only users who have authenticated and authorised get to even see the ClickOnce launch/install page.

Steps required:
First of all, in the project's ClickOnce properties, I change the default publish page from publish.html to publish.aspx.

Next, I added a modified web.config from one of my existing apps so that the forms authentication is enabled.

The IIS admin tool is used to make the app folder into a web app, and set the correct .NET version and add it to appropriate app pool.

That's it.

Each user is provided with the app's address page (or it can be made available from a centralised list of app's (menu) that I provide from the SQL DB). On attempting to access the page, if they are not already logged in, they get asked to do so and will only see the login form until they successfully login.

I also use the same credentials DB for logins within the ClickOnce app itself, so they must login to the app to create it's own security token (I use a custom class on the web service to perform this).

Al,
A good solution, however when ClickOnce is making its subsequent requests to the application references through the ASP.Net website they will be unprotected. If someone knew how, they could pick out each of these references and download the application without going through the intial login. In most cases, this is fine as you can have the downloaded application login, as you are doing. In our scenario, we wanted to protect the very download of those files themselves.

I have to admit I hadn't realised that the ClickOnce download process wouldn't operate after these extensions were added to the application mappings. Works great for protection of direct browser access, etc, but not for genuine users.

If the app is already installed, it'll still run, but it won't update, which kinda bypasses the whole point.....

If I understand you correctly, you are using a Forms Auth website, to gate your clickonce app, and only redirect to the app when they have logged in. The only problem is, what if someone finds out the url of your clickonce app? They could just hit that directly. If you aren't careful google etc. will spider your .application or .xbap and anyone will be able to hit them directly. The key here is to defend all of the ClickOnce application files behind forms authentication. If I understand you correctly, all you are achieving is obfuscating the location of the ClickOnce site.

To address the protection of files not normally covered by the ASP.NET forms auth engine, these can be added in the IIS extensions, using the Application Configuration dialogue, Mappings tab. Simply add the extensions that ClickOnce creates (deploy, manifest, application) using the same aspnet isapi as used for normal content.

Any attempt to access a known, described file (including the version sub-folder) will be met with the login request.

!! Update !!
See my reply to David above. This protection does work for direct access attempts, but it also breaks the ClickOnce download functions, so is a false starter, sorry!

Yeah, the key is that if you protect the other files with ASP.NET forms auth, IE doesn't flow the security session over after you've authenticated against the site. So ClickOnce doesn't know about the session information. Our solution the only way we've found to make ClickOnce hold and pass on this session information in the subsequent requests that it makes.

An important caveat here, is that this article is describing using ASP.Net forms authentication with an online ClickOnce application, specifically XBAP. The official line from Microsoft is that only Windows Trusted over the intranet is supported from ClickOnce. I know many people feel that this just doesn't allow for ClickOnce to be as great as it could be. I was hoping for an update to this in framework 3.5, but who knows.