Friday, October 27, 2017

REST with SSL

I wrote a post a few years ago about how to create a generic Proxy class to make it easy to consume REST web services:

http://geek-goddess-bonnie.blogspot.com/2014/06/proxy-class-for-consuming-restful.html

As I became aware of more functionality that I needed to add to my ProxyBaseREST class (see above post for the code for the class, abbreviated but useful as is), I realized that I should post the changes I made in a 2nd post:

http://geek-goddess-bonnie.blogspot.com/2014/08/revisiting-rest-proxy.html

In that post, I had added functionality for passing credentials (username/password) in the header of the call to the REST service, like this:

WebClient WC = new WebClient();
WC.Credentials = new NetworkCredential(this.UserName, this.Password);

This will work fine, but you may have noticed that I'm using the WC.Credentials. The upshot of using Credentials like this is .NET will make two calls to the REST service. This would dramatically increase your network traffic. Why does .NET do this? See this for an explanation:

https://stackoverflow.com/questions/6338942/why-my-http-client-making-2-requests-when-i-specify-credentials)

But then I solved that particular dilemma by using Basic Authorization ... of course, there's a little more to it than that!  Isn't that *always* the case?  ;0)  So, I added a static field to my ProxyBaseREST class (which you can change in an appSetting in your config file, depending on whether or not you need BasicAuthorization for any specific REST service implementation):

protected static bool UseBasicAuthorization = false;

public ProxyBaseREST(string baseAddress)
{
this.BaseAddress = baseAddress;
this.ReadAppSettings();
}
public ProxyBaseREST(string baseAddress, string userName, string password) : this(baseAddress)
{
this.UserName = userName;
this.Password = password;
}

protected void ReadAppSettings()
{
string setting = ConfigurationManager.AppSettings.Get("UseBasicAuthorization");
if (setting.IsNotNullOrEmpty() && setting.ToLower() == "true")
UseBasicAuthorization = true;
}

And, now we can handle our calls like this:

protected string GetJSON()
{
string json = "";
using (WebClient WC = new WebClient())
{
WC.Headers["Content-type"] = "application/json";
json = this.OpenWebClientAndRead(WC);
}
return json;
}
private string OpenWebClientAndRead(WebClient WC)
{
try
{
if (this.UserName.IsNotNullOrEmpty() && this.Password.IsNotNullOrEmpty())
{
if (UseBasicAuthorization)
WC.Headers[HttpRequestHeader.Authorization] = "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(this.UserName + ":" + this.Password));
else
WC.Credentials = new NetworkCredential(this.UserName, this.Password);
}

Stream stream = WC.OpenRead(this.BaseAddress + this.Parameters);

StreamReader reader = new StreamReader(stream);
string json = reader.ReadToEnd();
stream.Close();

if (json == "[]")
json = "";
return json;
}
catch (Exception ex)
{
// Log your error
return "";
}
}

But, sometimes there are problems with certificates, so I needed a way to ignore those Certificate errors (such as a non-signed certificate, out of date or other possible problems with the certificate). This can be done by setting up a validation callback. So, to do that, I needed to add two more static fields, and change the constructor yet again:

protected static bool IgnoreSslErrors = true;
protected static RemoteCertificateValidationCallback sslFailureCallback = null;

public ProxyBaseREST(string baseAddress)
{
this.BaseAddress = baseAddress;
if (sslFailureCallback == null) // static property, so this is only done once
{
sslFailureCallback = new RemoteCertificateValidationCallback(delegate { return true; });
this.ReadAppSettings();
if (IgnoreSslErrors)
ServicePointManager.ServerCertificateValidationCallback = sslFailureCallback;
}
}
protected void ReadAppSettings()
{
string setting;

setting = ConfigurationManager.AppSettings.Get("IgnoreSslErrors");
if (setting.IsNotNullOrEmpty() && setting.ToLower() == "false")
IgnoreSslErrors = false;

setting = ConfigurationManager.AppSettings.Get("UseBasicAuthorization");
if (setting.IsNotNullOrEmpty() && setting.ToLower() == "true")
UseBasicAuthorization = true;
}

I've set this up very simply, in that I'm ignoring any errors (by simply returning true from the ServerCertificateValidationCallback, any certificate will be allowed). There are better ways to handle this, but this simple code satisfied my needs. Here is what the MSDN documentation says:

https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.servercertificatevalidationcallback(v=vs.110).aspx

An application can set the ServerCertificateValidationCallback property to a method to use for custom validation by the client of the server certificate. When doing custom validation, the sender parameter passed to the RemoteCertificateValidationCallback can be a host string name or an object derived from WebRequest (HttpWebRequest, for example) depending on the CertificatePolicy property.

When custom validation is not used, the certificate name is compared with host name used to create the request. For example, if Create(String) was passed a parameter of "https://www.contoso.com/default.html";, the default behavior is for the client to check the certificate against www.contoso.com.

And, whether or not you need something more complicated than I've done, here is a StackOverflow post that may be helpful:

https://stackoverflow.com/questions/20914305/best-practices-for-using-servercertificatevalidationcallback

No comments:

Post a Comment