Pages

Monday, July 2, 2012

Using Basic Authentication In REST Based Services Hosted in IIS

So a colleague of mine asked a good question earlier today in reference to my last post on using Basic Authentication techniques in reference to REST based WCF services hosted in IIS. It turns out that there is conflicting documentation on whether or not a Custom User Name and Password Validator that has been configured works properly. In my last post I created a self-hosted service with full implementation and it does indeed work.

However best I can determine is that the IIS call stack is executed and handled, using Basic Authentication for your service does not allow you to override the IIS behavior and intercept those credentials using a custom username/password validator. This is because IIS is handling the authentication prior to the WCF service being called. Thus resulting in a lot of buzz around, "...why doesn't my custom user name and password validator" code get hit for a my WCF REST service hosted in IIS." I also believe I have found some definitive information on the support of this in IIS from Phil Henning's MSDN blog:

"In the version of WCF that shipped with .Net Framework 3.0 we didn't support custom validators with transport level HTTP security. We received much feedback from the community that this was a highly desired feature, so I'm happy to say we added support for this scenario in the 3.5 release of the .Net Framework. Note that this is only supported under self hosted services."

So that to me reads clearly. Also I found this tidbit from Yavor Georgiev of Microsoft confirming this and stating: "As the blog post mentions, this scenario is not supported while hosting in IIS. The reason is that IIS does the authentication before WCF receives the request." So there we have it - dual confirmation that the custom username/password validator is not supported in IIS hosted services.

In addition to this we have even yet another problem. Using Basic Authentication with REST based services hosted in IIS period. Even this vanilla security authorization technique is not supported. I didn't find much in the way of official documentation, but between my own failed tests, this forum post, and this blog post, it points in the direction that there is no 'out of the box' support for this combination.

At this point the easiest option if you need to use REST based services with a custom username/password validator and use Basic authentication may be to use a self-hosted service. I often hear and read, "Use an IIS based service unless there is a need to self-host." This is a scenario where self-hosting stands out as the winner... initially. But you know I wouldn't post without an IIS solution, right!

We do have another workable and legitimate solution to have a REST based service hosted in IIS using Basic Authentication. As I mentioned before in a few posts and shown examples for, we can implement our own CustomAuthorizationManager that inherits from ServiceAuthorizationManager and configure this for our service. This method is perfect for service level Authorization to your RESTful service. The best part is we can still inspect the incoming message headers to siphon out the client's passed credentials. In this manner we can still provide service level authorization.

As far as the 'Basic' authentication handling, we are going to need to do that ourselves. We can easily send back a response header to challenge for Basic Authentication credentials, and just have IIS wired up to "Anonymous Authentication." We are going to just let IIS do the hosting and take care of all of the authentication ourselves. After all IIS is just a huge, sophisticated wrapper in itself handling security (and a million other things) for us, so we will just shift control of this specific piece back to our service.

To begin, go ahead and configure the service to use a custom authorization manager, and point it to our class named: 'CustomAuthorizationManager'. Keep in mind that the format is [Assembly Namespace].[Classname], [Assembly Namespace] for the serviceAuthorizationManagerType value. Here is the configuration I used:

<serviceBehaviors>
<behavior name="SecureRESTSvcTestBehavior">
<serviceMetadata httpGetEnabled="false" httpsGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true.
Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceAuthorization serviceAuthorizationManagerType="RESTfulSecurityIIS.CustomAuthorizationManager, RESTfulSecurityIIS" />
</behavior>
</serviceBehaviors>

Next, let's build out our class that will override the 'CheckAccessCore' method. This allows us to intercept calls early in the call stack and decide after analyzing things like the user context or request headers if we want the call to succeed. This method simply returns a Boolean. Returning 'false' will prevent the call from continuing. The code I use below is not completely filled out for production. You probably want to test to see if the header contains "Basic" within 1st, and also make sure all the credentials are supplied. If not, returning a proper .NET exception would be in order. Here is the code I used:

protected override bool CheckAccessCore(OperationContext operationContext)
{
//Extract the Authorization header, and parse out the credentials converting the Base64 string:
var authHeader = WebOperationContext.Current.IncomingRequest.Headers["Authorization"];
if ((authHeader != null) && (authHeader != string.Empty))
{
var svcCredentials = System.Text.ASCIIEncoding.ASCII
.GetString(Convert.FromBase64String(authHeader.Substring(6)))
.Split(':');
var user = new { Name = svcCredentials[0], Password = svcCredentials[1] };
if ((user.Name == "user1" && user.Password == "test"))
{
//User is authrized and originating call will proceed
return true;
}
else
{
//not authorized
return false;
}
}
else
{
//No authorization header was provided, so challenge the client to provide before proceeding:
WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"MyWCFService\"");
//Throw an exception with the associated HTTP status code equivalent to HTTP status 401
throw new WebFaultException("Please provide a username and password", HttpStatusCode.Unauthorized);
}
}

Looking above, I siphon out the "Authorization" request header value to inspect. This is just a Base64 encoded string, so we can just convert it back. This is the very reason why we need to secure our service with a SSL certificate because the credentials are not secure. Once I parse out the username and password I can use the same tests I did before when using a custom username/password validator for self-hosted services. You can use the identical test calling code that I used in the last post to add the basic authentication credentials to the request header.

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(@"https://DevMachine1234:8099/MyRESTServices/Customer/1");
//Add a header to the request that contains our credentials
//DO NOT HARDCODE IN PRODUCTION!! Pull credentials real-time from database or other store.
string svcCredentials = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes("user1"+ ":" + "test"));
req.Headers.Add("Authorization", "Basic " + svcCredentials);
//Just some example code to parse the JSON response using the JavaScriptSerializer
using (WebResponse svcResponse = (HttpWebResponse)req.GetResponse())
{
using (StreamReader sr = new StreamReader(svcResponse.GetResponseStream()))
{
JavaScriptSerializer js = new JavaScriptSerializer();
string jsonTxt = sr.ReadToEnd();
}
}

At this point we have the Basic Authentication credentials coming into our service and we are providing authorization based on the outcome. However, like I mentioned prior we now are responsible for enforcing Basic Authentication to be used. The way we can do this is to send a response header prompting for credentials (which will happen in a browser) if they are not provided. In our code above, we are alerted of this condition when the Authorization header is not present. This in turn sends the client back a challenge for credentials. After submitting valid credentials, they will be authorized to our service. Just keep in mind again, since we are handling all authentication and enforcement of security mode in Basic Authentication, you will configure IIS to use "Anonymous Authentication."


So we still can use Basic Authentication with IIS hosted REST services using the webHttpBinding. Once this all sinks in and you test the code, you will see how all the parts come together. You have a choice at least with self-hosting or IIS with REST services and in both environments we have workable options. Since Basic Authentication is a HTTP standard widely known, and not some specific .NET practice or implementation, it is an obvious choice for securing your services. I would have to believe there be some improved support for WCF REST services in IIS going forward, but in the meantime we do have legitimate workable solutions for this hosting scenario.

0 comments:

Post a Comment