I've seen many posts about this issue while researching but never saw a great solution. So, I'm posting mine here in hopes that it helps.
In short, I have an application making a request to another application to generate a PDF from a URL. Once I upgraded to a more recent version of EO.Pdf, this process resulted in a PDF of a 401.2 error. That’s because we use Windows authentication on this site. EO.Pdf’s underlying webrequest no longer has “usedefaultcredentials” set to true. In fact, it does not appear to actually set credentials at all.
The standard transaction for this type of authentication is: request from a client, 401.2 response from server with headers identifying acceptable authentication options, a request from client with a proper authentication header. By not setting credentials, the underlying webrequest simply stops at the 401.2. So EO.Pdf can either render that response as PDF (oddly the default) or throw an error because it did not get a 2xx response code (I think that’s their logic, but I wouldn’t know exactly how they have that set up). What’s even more frustrating, the exception thrown by EO.Pdf doesn't include a clean way to get the actual non-2xx response code for handling.
I used a few tools on my web server to watch the requests/responses and have verified that EO.Pdf never follows up to the 401.2 response. It never attempts to send an authentication header at all. It’s unfortunate, because the webRequest object in .NET is designed to abstract all of that lower level authentication headache from the developer. Oh well, I decided to jump in and make my own authentication header.
The potential flaw with this, which I believe to be inherent in the way EO.Pdf has abstracted the underlying request and response, is that you can’t reliably/cleanly detect a 401.2 error (step 1 response in a normal interaction). That means you may need to create two methods. One that makes your request assuming Windows authentication and another that assumes no authentication.
Code: C#
private static string GetNegotiateAuthHeader(string targetUrl, int tokenTimeoutMinutes = 1)
{
// get uri object for targetUrl to resolve both host and absoluteUri
var requestUri = new Uri(targetUrl);
// this is the standard location to store SPN. If this one is not in there, set it.
// If you instantiate a webrequest and properly set the credentials (usedefaultcredential = true, for example), then perform the request,
// the webrequest object will automatically retry after the intial 401.2 response and set this same information for use in subsequent requests.
// We are just following that model since we cannot control the behavior of EO.Pdf's underlying webrequest objects.
if (!AuthenticationManager.CustomTargetNameDictionary.ContainsKey(requestUri.AbsoluteUri))
{
// dns lookup to get the actual hostName from the url.
var hostentry = Dns.GetHostEntry(requestUri.Host);
// add it here. MSDN says the key should be the absoluteUri, so we're sticking to their recommendation.
AuthenticationManager.CustomTargetNameDictionary.Add(requestUri.AbsoluteUri, $"HTTP/{hostentry.HostName}");
}
// instantiate tokenProvider using SPN
var provider = new KerberosSecurityTokenProvider(AuthenticationManager.CustomTargetNameDictionary[requestUri.AbsoluteUri]);
// the actual token to be used
KerberosRequestorSecurityToken token = (KerberosRequestorSecurityToken)provider.GetToken(new TimeSpan(0, tokenTimeoutMinutes, 0));
// base64 encoding of the value that can be used in the header of the http request
string initialContextToken = Convert.ToBase64String(token.GetRequest());
// properly formatted http Authorization header value for kerberose authentication.
return $"Authorization: Negotiate {initialContextToken}";
}
That method could be used with EO.Pdf as follows:
Code: C#
public static void EoTesting(string url, string pdfPath)
{
var options = new HtmlToPdfOptions() { AdditionalHeaders = new string[] { GetNegotiateAuthHeader(url) } };
HtmlToPdf.ConvertUrl(url, pdfPath, options);
}