How to accommodate Authenticating Proxies in .NET

In some environments network administrators have chosen to require users to authenticate through their proxy servers.  I will give you a good starting point to code your application to deal with authenticating proxies.  You can make the decision of how and where you would like to cache these proxy credentials if you wish to.

A quick overview of how the .NET framework handles proxies

The .NET framework by default will discover the proxy as long as you are using WPAD in your environment or you have hard coded the proxy and port in the Internet Proxy settings.  You cannot point to a .pac file however (this has been true for many years incidentally).  The .NET framework does not automatically send the default user credentials or prompt you for credentials if the proxy challenges the HTTP request.  It is up to the programmer to accommodate the HTTP proxy authentication challenge.  You can see this challenge by taking a network trace or by using Fiddler.  The HTTP status code 407 is the proxy authentication challenge.  If your application is using XMLHttpRequest (such as the WinJS.xhr object in a Windows Store app) the underlying WinINet implementation will take care of prompting for credentials if necessary, and you do not have to do anything yourself to get past an authenticating proxy.

Code sample and discussion

I chose to show an example in a simple Windows Store app.  This app is written in C#.  The code is fairly well documented but here are some other points you will find important:

  • This is only sample code to get you started.  There is no warrantee or guarantee associated with this sample it is up to you, the developer, to ensure this will work for your particular application.
  • See my notes in the code about trying to authenticate with the Digest or Basic protocol.  I would not be happy if you blasted my credentials in clear text.  At least make it configurable for you app!
  • This code does not handle authenticating through proxies that are chained and each requiring authentication.  This is a weird edge case that should never occur in the real world, but I know someone will comment about this if I didn’t include this note.
  • This code will try using the logged on user’s credentials first.  By far this SHOULD be the most common case you encounter in the wild.
  • This code then establishes a credential cache to use for the duration of the app lifetime and will attempt to get the correct credentials from the user.  I allowed three retries in my code which is pretty standard (in the event the user incorrectly enters the password or user id).
  • I included code to use the most secure authentication protocol returned, and do NOT retry less secure protocols.  You may want to track and retry the less secure protocols yourself.  That would be unusual though and most clients do not do that in the real world.
  • To try this code, create a blank C# Windows Store app called ‘ProxyAuthentication’.  Then MAKE SURE you change the capabilities in the manifest to include Enterprise Authentication and Private Networks.  Then replace the MainPage.xaml and MainPage.xaml.cs content with the following code:

Code:

MainPage.xaml

<Page
    x:Class="ProxyAuthentication.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ProxyAuthentication"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        < StackPanel HorizontalAlignment="Left" Height="100" Margin="120,96,0,0" VerticalAlignment="Top">
         < TextBlock Text="Proxy authentication!"/>  
            <Button Name="BtnRequest" Content="Push me    " Click="BtnRequest_Click"></Button>
            < TextBlock Name="TxtOut" TextWrapping="Wrap" Text="TextBlock"/>
        </StackPanel>

    </Grid>
< /Page>

MainPage.xaml.cs

    

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Security.Credentials.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

namespace ProxyAuthentication
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        // global http client to make requests;
        HttpClient appHttpClient;
        // global credential cache for proxy credentials;
        CredentialCache proxyCredCache;

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }

        private async void BtnRequest_Click(object sender, RoutedEventArgs e)
        {
            //hard coded for my testing!
            string result = await DoRequestAsync("https://bing.com");
            //TODO:  possibly log the result string for troubleshooting
        }

        private async Task<HttpResponseMessage> TryDomainCredentialsAsync(string theUri)
        {
            //set the proxy credentials to the Currently Logged on user credentials
            WebRequest.DefaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
            return await appHttpClient.GetAsync(theUri);
        }

        private async Task<HttpResponseMessage> TryCachedCredentialsAsync(string theUri)
        {
            // try the global credential cache I created earlier!
            WebRequest.DefaultWebProxy.Credentials = proxyCredCache;
            return await appHttpClient.GetAsync(theUri);
        }

        // This will pop up a credenital dialog.  It could use smart cards if that is supported by the proxy (they will just show up here)
        private async Task<HttpResponseMessage> TryCredentialPickerAsync(string theUri, AuthenticationHeaderValue header)
        {
            // Basic and Digest credentials are easily decoded.
            // You may want to test header.Scheme and see if it is Digest or Basic
            // and warn the end user that they are about to supply their credentials over clear text
           
            var proxy = WebRequest.DefaultWebProxy.GetProxy(new Uri(theUri));
            CredentialPickerOptions credPickerOptions = new CredentialPickerOptions();
            credPickerOptions.TargetName = WebRequest.DefaultWebProxy.GetProxy(new Uri(theUri)).DnsSafeHost;
            credPickerOptions.Message = "Proxy Authentication required for: " + credPickerOptions.TargetName;
            credPickerOptions.Caption = "Please enter your Proxy credentials";
            credPickerOptions.CallerSavesCredential = false;
            credPickerOptions.CredentialSaveOption = CredentialSaveOption.Hidden;
            credPickerOptions.AuthenticationProtocol = GetAuthEnum(header.Scheme);
            CredentialPickerResults credPickerResults = await CredentialPicker.PickAsync(credPickerOptions);

            if (proxyCredCache == null)
            {
                proxyCredCache = new CredentialCache();
            }

            // see if credentials already exist and remove if they do!
            var existingCred = proxyCredCache.GetCredential(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString());
            if (existingCred != null)
            {
                proxyCredCache.Remove(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString());
                existingCred = null;
            }

            // add the credentials entered to the cache
            proxyCredCache.Add(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString(), new NetworkCredential(credPickerResults.CredentialUserName, credPickerResults.CredentialPassword));

            // try the global credential cache I created!
            WebRequest.DefaultWebProxy.Credentials = proxyCredCache;

            return await appHttpClient.GetAsync(theUri);
        }

        //Helper function to translate a string to the authorization enum we need.
        //There are other protocols but I could not test them so I will not include them.
        private AuthenticationProtocol GetAuthEnum(string theVal)
        {
            AuthenticationProtocol returnVal = AuthenticationProtocol.Basic;

            switch (theVal.ToLower())
            {
                case "basic":
                    {
                        returnVal = AuthenticationProtocol.Basic;
                        break;
                    };
                case "digest":
                    {
                        returnVal = AuthenticationProtocol.Digest;
                        break;
                    };
                case "negotiate":
                    {
                        returnVal = AuthenticationProtocol.Negotiate;
                        break;
                    };
                case "ntlm":
                    {
                        returnVal = AuthenticationProtocol.Ntlm;
                        break;
                    };
                case "kerberos":
                    {
                        returnVal = AuthenticationProtocol.Kerberos;
                        break;
                    };
            };

            return returnVal;
        }

        // simple helper to compare security of protocols
        private bool newMoreSecure(AuthenticationHeaderValue currVal, AuthenticationHeaderValue newVal)
        {
            return GetAuthEnum(newVal.Scheme) > GetAuthEnum(currVal.Scheme);
        }

        // from the proxy auth headers, determing the most secure authentication and return it.
        private AuthenticationHeaderValue ProxyAuthType(HttpHeaderValueCollection<AuthenticationHeaderValue> headers)
        {
            AuthenticationHeaderValue currHeader = null;

            foreach (AuthenticationHeaderValue header in headers)
            {
                if (currHeader == null)
                {
                    currHeader = header;
                }
                else
                {
                    if (newMoreSecure(currHeader, header))
                    {
                        currHeader = header;
                    }
                }
            }

            return currHeader;
        }

        // Kick off a simple Get request, accomodate proxy authentication
        private async Task<string> DoRequestAsync(string theUri)
        {
            HttpResponseMessage theResponse = null;
            string returnTxt = "";

            if (appHttpClient == null)
            {
                appHttpClient = new HttpClient();
            }

            try
            {
                theResponse = await appHttpClient.GetAsync(theUri);
                bool retry = true;
                bool triedDomainCreds = false;
                bool triedCachedCreds = false;

                int retryCount = 3;

                // if 407 was returned so we need to do some proxy authentication
                while ((theResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired) && (retry == true))
                {
                    // find out what type of auth would be the most secure:
                    AuthenticationHeaderValue header = ProxyAuthType(theResponse.Headers.ProxyAuthenticate);

                    if (header != null)
                    {
                        // if possible to silently use domain credentials then try that first but only try once! 
                        // We will loop back and use the credenial picker the next time if necessary
                        // Note:
                        // Basic and Digest credentials are easily decoded.
                        // You may want to test header.Scheme and see if it is Digest or Basic
                        // and warn the end user that they are about to supply their credentials over clear text.
                        // In this case I decided NEVER to allow the default credentials to be passed if Digest or Basic auth is used
                        // If the HTTP proxy requests BASIC auth because it is setup that way by the admin, there is not much we can do about it
                        // but I choose in my case not to silently use the default credentials with these less secure schemes
                        if ((triedDomainCreds == false) && (GetAuthEnum(header.Scheme) > AuthenticationProtocol.Digest))
                        {
                            try
                            {
                                theResponse = await TryDomainCredentialsAsync(theUri);
                            
                            }
                            catch(Exception domainTryExeption)
                            {
                                // TODO: log this exception
                                returnTxt = domainTryExeption.Message;
                                // edge case.  If you don't have Enterprise Authentication in your manifest
                                // (store policy may not allow your app to have this)
                                // you cannot use the current user credentials, so prompt for them instead
                            }
                            triedDomainCreds = true;
                        }
                        else
                        {
                            // see if we have credentials stored for this.
                            if ((triedCachedCreds == false) && (proxyCredCache != null))
                            {
                                theResponse = await TryCachedCredentialsAsync(theUri); // try them
                                triedCachedCreds = true;
                            }
                            else // last resort, try the credential picker!
                            {
                                theResponse = await TryCredentialPickerAsync(theUri, header);
                                retryCount--; // only try to get the creds retryCount times, then bail
                                // TODO:  You probably want to delete the credential cache entry you just tried since if it did not work
                            }
                        }

                        // did we authorize through the proxy?
                        if (theResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
                        {
                            // should we retry?
                            if (retryCount == 0)
                            {
                                retry = false;
                            }
                        }
                        else
                        {
                            // all other statuses return stop proxy auth
                            retry = false;
                        }
                    }
                    else
                    {
                        retry = false;
                        returnTxt = "Problem, expected and Authorization header and did not find one!";
                    }
                }
               
            }
            catch (Exception theEx)
            {
                // TODO: some other problem so report it possibly to help the customer or log it for your own debugging!
                returnTxt = theEx.Message;
            }

            if (theResponse != null)
            {
                returnTxt = theResponse.StatusCode.ToString();   
            }
            TxtOut.Text = returnTxt;
           
            return returnTxt;
        }
    }
}

 

Summary

I hope that this give you some insight into how authenticating proxies work and how you can accommodate them.  Again, this code is just to get you started.  You should test your application with different proxy environments before shipping!

Note: Windows Store JavaScript/HTML5 apps and .NET apps written with Windows.Web.Http will automatically prompt for Proxy Credentials.

Let me know if this helped you out in any way!

Peace,

Jeff