Как получить учетные данные для проверки подлинности прокси в .Net Desktop App (альтернатива CredentialPicker) - PullRequest
0 голосов
/ 05 ноября 2018

Я разрабатываю настольное приложение WPF, для которого требуется доступ в Интернет. Некоторые сетевые среды требуют прокси-аутентификации. Используя эту запись в блоге MSDN Мне удалось реализовать перехват ошибки 407, возвращаемой прокси-сервером, и использовать DomainCredentials в качестве запасного варианта. Однако я не могу реализовать CredentialPicker, использованный в статье блога, так как это из библиотеки UWP (), на которую я не могу ссылаться изначально. Я должен предоставить пользователю своего рода диалоговое окно для ввода своих учетных данных для аутентификации через прокси-сервер, если они отличаются от DefaultCredentials (хотя, я не уверен, как часто возникает этот пограничный случай и стоит ли оно усилий ).

  • Могу ли я в любом случае использовать библиотеку UWP, например, если я скопирую это откуда-то?
  • Или в .Net Framework есть эквивалент?
  • Или есть сторонняя библиотека, которую я еще не нашел?
  • Или я должен реализовать это самостоятельно?

Я бы предпочел не реализовывать это самостоятельно, поскольку в будущем может потребоваться поддержка смарт-карт или некоторых других методов аутентификации, а не просто комбинация имени пользователя и пароля.

Выдержка из связанной статьи блога

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 http://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("http://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; 
        } 
    } 
}
...