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:
NTLM, Negotiate
NTLM Auth
and receives a NTLM WWW-Authenticate
Browsing with Chrome 69.0.3497.100 fails:
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:
Software\Policies\Google\Chrome\
registry aswhich all in all did nothing: I still do not get any SSO using Chrome:
What I tried to get it working inside CefSharp:
CefSharp.Handler.DefaultRequestHandler
, overriding
OnSelectClientCertificate
-> never gets calledOnCertificateError
-> no longer gets calledGetAuthCredentials
-> 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.Cef.Initialize(...)
that containsvar settings = new CefSettings { IgnoreCertificateErrors = true, ... };
settings.CefCommandLineArgs.Add ("auth-server-whitelist", "*host-url*");
settings.CefCommandLineArgs.Add ("auth-delegate-whitelist", "*host-url*");
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?
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:
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;
}