Wednesday 10 June 2009

ClickOnce updates via authentication proxy

(Disclaimer: This is my first blog post)

You may be aware that ClickOnce installation and updating is not supported via proxy servers that require authentication credentials other than the users default credentials.

http://social.msdn.microsoft.com/Forums/en-US/winformssetup/thread/82be8797-f14f-4db1-acb8-3206881cc567.

The thread suggests that there is hot fix but as far as I am aware this does not fix the issue which is also highlighted at the end of the thread.

I came across this very issue working on a project for a client who required the application we were writing for them to run inside various different environments using varying proxy servers and configurations. In order to accommodate this we allowed the user to either use the proxy settings provided in IE or to be able to override this with custom proxy settings. This worked fine for WCF, HTTP calls and even for creating BITS jobs but we were unable to get ClickOnce updates working.

And it was all because of this little gem in the System.Deployment.SystemNetDownloader:

protected void DownloadSingleFile(FileDownloader.DownloadQueueItem next)
{
    ...
 
    IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
 
    if (defaultWebProxy != null)
    {
        defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
    }
 
    ...
}

This means that every time .Net tries to download a file via the SystemNetDownloader it is overwriting the credentials on the default web proxy with the users default credentials.

In order to work around this short sighted implementation I created a wrapper around the WebProxy object which implemented the IWebProxy interface, the important part here was to specifically do nothing with the Set on the Credentials property and provide a alternative way of setting the proxy credentials:

public class CustomWebProxy : IWebProxy
{
    private WebProxy webProxy;
 
    public CustomWebProxy(string serverName, int port)
    {
        webProxy = new WebProxy(serverName, port);
        webProxy.UseDefaultCredentials = false;
    }
 
    public ICredentials Credentials
    {
        get { return webProxy.Credentials; }
        set { } //The important part
    }
 
    public void SetCredentials(ICredentials credentials){ … }
    public bool BypassProxyOnLocal { … }
    public Uri GetProxy(Uri destination) { … }
    public bool IsBypassed(Uri host) { … }
    public bool UseDefaultCredentials { … }
    public Uri Address { … }
}

The final piece of the puzzle is to setup the proxy, the following should be placed at an appropriate place in the application workflow:

var networkCredential = new NetworkCredential("UserId", "Password");
var proxy = new CustomWebProxy("server", 8080);
proxy.BypassProxyOnLocal = false;
proxy.SetCredentials(networkCredential);
WebRequest.DefaultWebProxy = proxy;

It is worth pointing out that this only solves the issue with ClickOnce and proxy authentication once the application is installed you will still need to find away to get the application on to the users computer first, distribute by CD for example.

Bro Num

16 comments:

  1. Excellent post , keep it up

    ReplyDelete
  2. Thanks for your workaround. It does not work if the client machine has to use the authenticate proxy to get the update from server. Can you please give me any idea to use your workaround in my scenario?

    ReplyDelete
  3. The above solution worked for us but we have only tried it against our customers proxy which requires a username and password other than the users domain credentials.

    I am assuming that client machine has the clickonce app installed via another method and that the update is done in code.

    It is important to remember that the setter on the Credentials property should do nothing and that it is replaced by a SetCredentials method which takes an ICredentials object. You also need to wire up the wrappers properties to the encapsulated WebProxy object.

    ReplyDelete
  4. Hi,

    Re "the following should be placed at an appropriate place" - whereabouts should it go? Can you explain how clickonce starts up and how/when this Microsoft clickonce code hits (a) the code you've posted, separately to (b) your own app code. Like is the code you've posted supposed to be called by the clickonce code somehow, or by your own application code. I'm assuming it's not the latter as clickonce has to check for updates before it runs your own code.

    ReplyDelete
  5. Greg, My code only works if you are calling the ClickOnce update from within your own application and you are not relying on the basic ClickOnce update functionality. Because of this you would need to install the application on the users machine via another method such as CD which click once supports.

    In order to do this you will need to have a look at the ClickOnce api (here is a good place to start http://msdn.microsoft.com/en-us/library/wh45kb66(VS.90).aspx).

    So in answer to your question the appropriate place is before you make your call to the ClickOnce Api to check for an update.

    ReplyDelete
  6. Thanks for figuring this out and posting it. This problem has been vexing people for a long time. I've posted a link to this article in the long proxy-authentication thread in the MSDN ClickOnce forum, and someone has verified that it does, indeed, work. http://social.msdn.microsoft.com/Forums/en/winformssetup/thread/3e9cebad-9630-4bbc-a0ca-0d2f20335454
    I'm also going to post this on my ClickOnce blog for people to reference.
    RobinDotNet
    Microsoft MVP, Client App Dev

    ReplyDelete
  7. Glad it was of some help. It was a very frustrating but rewarding problem to solve and was the catalyst to get me to start writing a blog.

    Also interested in your ClickOnce blog, some interesting stuff in there, you have a new follower.

    ReplyDelete
  8. Very nice find and creative idea to work around the issue. It works really nice. Thank you very much!

    BTW - it works well, no matter if the installation was done online or offline. You need to enable "online update" during publish process.

    However, you have to disable the automagical update mechanism of Click Once at startup of you app and call CheckForUpdate and Update yourself.

    ReplyDelete
  9. Is there a full source code to download for this workaround?

    ReplyDelete
  10. @Zekljo. Due to the simplicity of the wrapper and where the hook needs to be placed will differ depending on peoples implementation I have not supplied any source code. Give it a try, if you are still having problems let me know. Remember you only have to do two things:

    1 - Create a proxy wrapper where each method call apart from the Credentials property should be passed through to the inner proxy.

    2 - Wire up the DefaultWebProxy

    ReplyDelete
  11. What should I put inside these brackets "{ … }" ?

    ReplyDelete
  12. @Josip All you are doing is wrapping up the inner proxy so each method just passes the call through:

    public void SetCredentials(ICredentials credentials)
    {
    webProxy.SetCredentials(credentials);
    }
    public bool BypassProxyOnLocal
    {
    get
    {
    return webProxy.BypassProxyOnLocal;
    }
    get
    {
    webProxy.BypassProxyOnLocal = value;
    }
    }

    etc...

    ReplyDelete
  13. Your site is for sure worth bookmarking.
    Proxy for Facebook

    ReplyDelete
  14. It's 2013 and this bug still exists in Win7 / .NET 4.5

    Impressive.

    ReplyDelete
  15. It's 2016 and this bug still exists in Win7 / .NET 4.0

    Impressive.

    ReplyDelete