Search code examples
c#google-chromecefsharp

How to fix SSO connect with cefSharp offline browser to webserver hosted by self-signed PC?


TL;DR: I am grasping for straws here, anybody got a SSO with CefSharp working and can point me to what I am doing wrong? I try to connect to a SSL-SSO page through CefSharp but it wont work - neither does it in Chrome-Browser. With IE it just works. I added the to trusted sites (Proxy/Security), I tried to tried to whitelist-policy the URL for chrome in the registry and tried different CefSharp settings - nothing helped.


I am trying (to no avail) to connect to a SSO enabled page via CefSharp-Offline-browsing.

Browsing with normal IE it just works:

  • I get 302 answer
  • the redirected site gives me a 401 (Unauthorized) with NTLM, Negotiate
  • IE automagically sends the NTLM Auth and receives a NTLM WWW-Authenticate
  • after some more 302 it ends in 200 and a logged in state on the website

Browsing with Chrome 69.0.3497.100 fails:

not secure

I guess this is probably due to the fact that the webserver is setup on a co-workers PC and uses a self-signed cert.


F12-Debugging in IE/Chrome:

  • In IE I see a 302, followed by two 401 answers, and end on the logged in site.

  • In chrome I see only 302 and 200 answers and end on the "fallback" login site for user/pw entry.

The main difference in (one of the 302) request headers is NEGOTIATE vs NTLM

// IE:
Authorization: NTLM TlRMT***==

// Chrome:
Authorization: Negotiate TlRMT***==
Upgrade-Insecure-Requests: 1
DNT: 1

No luck to connect through CefSharp so far, I simply land in its RequestHandler.GetAuthCredentials() - I do not want to pass any credentials with that.


What I tried to get it working inside Windows / Chrome:

which all in all did nothing: I still do not get any SSO using Chrome: not secure


What I tried to get it working inside CefSharp:

  • deriving from CefSharp.Handler.DefaultRequestHandler, overriding
    • OnSelectClientCertificate -> never gets called
    • OnCertificateError -> no longer gets called
    • GetAuthCredentials -> gets called, but I do not want to pass login credentials this way - I already have a working solution for the http:// case when calling the sites normal login-page.
  • providing a settings object to Cef.Initialize(...) that contains
var settings = new CefSettings { IgnoreCertificateErrors = true, ... };
settings.CefCommandLineArgs.Add ("auth-server-whitelist", "*host-url*");
settings.CefCommandLineArgs.Add ("auth-delegate-whitelist", "*host-url*");
  • on creation of the browser providing a RequestContext:
var browser = new CefSharp.OffScreen.ChromiumWebBrowser (
    "", requestContext: CreateNewRequestContext (webContext.Connection.Name));

CefSharp.RequestContext CreateNewRequestContext (string connName)
{
var subDirName = Helper.Files.FileHelper.MakeValidFileSystemName (connName);
var contextSettings = new RequestContextSettings
{
PersistSessionCookies = false,
PersistUserPreferences = false,
CachePath = Path.Combine (Cef.GetGlobalRequestContext ().CachePath, subDirName),
IgnoreCertificateErrors = true,
};
// ...
return new CefSharp.RequestContext (contextSettings);
}

I am aware that part of those changes are redundant (f.e. 3 ways to set whitelists of which at least 2 should work for CefSharp, not sure about the registry one affecting it) and in case of IgnoreCertificateErrors dangerous and can't stay in. I just want it to work somehow to then trim back what to do to make it work in production.


Research:

and others .. still none the wiser.


Question: I am grasping for straws here , anybody got a SSO with CefSharp working and can point me to what I am doing wrong?


Solution


  • TL;DR: I faced (at least) 2 problems: invalid SSL certificates and Kerberos token problems. My test setup has local computers set up with a web-server I call into. These local computers are mostly windows client OS VMs with self-signed certificates. Some are windows servers. The latter worked, the fromer not. With IE both worked.


    Browsing to the site in question using https://... lead to CEFsharp encountering the self-signed certificate (which is not part of a trusted chain of certs) - therefore it will call the browsers RequestHandler (if set) and call into its

    public override bool OnCertificateError (IWebBrowser browserControl, IBrowser browser, 
                                             CefErrorCode errorCode, string requestUrl, 
                                             ISslInfo sslInfo, IRequestCallback callback)
    {
      Log.Logger.Warn (sslInfo.CertStatus.ToString ());
      Log.Logger.Warn (sslInfo.X509Certificate.Issuer);
    
      if (CertIsTrustedEvenIfInvalid (sslInfo.X509Certificate))
      {
        Log.Logger.Warn ("Trusting: " + sslInfo.X509Certificate.Issuer);
    
        if (!callback.IsDisposed)
          using (callback)
          {
            callback?.Continue (true);
          }
    
        return true;
      }
      else
      {
        return base.OnCertificateError (browserControl, browser, errorCode, requestUrl,
                                        sslInfo, callback);
      }
    }
    

    For testing purposes I hardcoded certain tests into CertIsTrustedEvenIfInvalid (sslInfo.X509Certificate) that would return true for my test environment - this might be replaced by a simple return false, an UI-Popup presenting the cert and asking the user if she wants to proceed or it might take certain user-provided cert-files into account - dunno yet:

    bool CertIsTrustedEvenIfInvalid (X509Certificate certificate)
    {
      var debug = new Dictionary<string, HashSet<string>> (StringComparer.OrdinalIgnoreCase)
      {
        ["cn"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "some", "data" },
        ["ou"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "other", "stuff" },
        ["o"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "..." },
        ["l"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Atlantis" },
        ["s"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Outer Space" },
        ["c"] = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "whatsnot" },
      };
    
      var x509issuer = certificate.Issuer
        .Split (",".ToCharArray ())
        .Select (part => part.Trim().Split("=".ToCharArray(), 2).Select (p => p.Trim()))
        .ToDictionary (t => t.First (), t => t.Last ());
    
      return x509issuer.All (kvp => debug.ContainsKey (kvp.Key) &&
                                    debug[kvp.Key].Contains (kvp.Value));
    }
    

    Only if the SSL-Step works, SSO will be tried.


    After solving the SSL issue at hand I ran into different behavious of Chrome versus IE/Firefox etc as described here @ Choosing an authentication scheme - the gist of it is:

    • if multiple auth schemes are reported by the server, IE/Firefox use the first one they know - as delivered by the server (preference by order)
    • Chrome uses the one which it deems of highest priority (in order: Negotiate -> NTLM -> Digest->Basic) ignoring the servers ordering of alternate schemes.

    My servers reported NTLM,Negotiante (that order) - with IE it simply worked.

    With Chrome this led to Kerberos tokens being exchanged - which only worked when the web-server was hosted on a Windows Server OS - not for Windows Client OS. Probably some kind of failed configuration for Client-OS computers in the AD used. Not sure though - but against Server OS it works.

    Additionaly I implemented the

    public override bool GetAuthCredentials (IWebBrowser browserControl, IBrowser browser, 
                                             IFrame frame, bool isProxy, string host, 
                                             int port, string realm, string scheme, 
                                             IAuthCallback callback)
    {
      // pseudo code - asks for user & pw 
      (string UserName, string Password) = UIHelper.UIOperation (() =>
      {
        // UI to ask for user && password: 
        // return (user,pw) if input ok else return (null,null)
      });
    
      if (UserName.IsSet () && Password.IsSet ())
      {
        if (!callback.IsDisposed)
        {
          using (callback)
          {
            callback?.Continue (UserName, Password);
          }
          return true;
        }
      }
    
      return base.GetAuthCredentials (browserControl, browser, frame, isProxy, 
                                      host, port, realm, scheme, callback);
    }
    

    to allow for a fail-back if the SSO did not work out. After providing the AD credentials in this dialog login is possible as well).

    For good measure I also whitelisted the hosts to the CEF-Browser context on creation of a new broswer like so:

    CefSharp.RequestContext CreateNewRequestContext (string subDirName, string host,
                                                     WebConnectionType conType)
    {
      var contextSettings = new RequestContextSettings
      {
        PersistSessionCookies = false,
        PersistUserPreferences = false,
        CachePath = Path.Combine (Cef.GetGlobalRequestContext ().CachePath, subDirName),
      };
    
      var context = new CefSharp.RequestContext (contextSettings);
    
      if (conType == WebConnectionType.Negotiate) # just an enum for UserPW + Negotiate
        Cef.UIThreadTaskFactory.StartNew (() =>
         {
           // see https://cs.chromium.org/chromium/src/chrome/common/pref_names.cc  for names
    
           var settings = new Dictionary<string, string>
           {
             ["auth.server_whitelist"] = $"*{host}*",
             ["auth.negotiate_delegate_whitelist"] = $"*{host}*",
             // only set-able via policies/registry :/
             // ["auth.schemes"] = "ntlm" // "basic", "digest", "ntlm", "negotiate"
           };
    
           // set the settings - we *trust* the host with this and allow negotiation
           foreach (var s in settings)
             if (!context.SetPreference (s.Key, s.Value, out var error))
               Log.Logger.Debug?.Log ($"Error setting '{s.Key}': {error}");
         });
    
      return context;
    }