If you secure an OData Service using Windows authentication – see Part 2 to learn how – everything works as expected out of the box.
What however if you need a different authentication scheme?
Well the answer as always depends upon your scenario.
Broadly speaking what you need to do depends upon how your Data Service is hosted. You have three options:
- Hosted by IIS
- Hosted by WCF
- Hosted in a custom host
But by far the most common scenario is…
Hosted by IIS
This is what you get when you deploy your WebApplication project – containing a Data Service – to IIS.
At this point you have two realistic options:
- Create a custom HttpModule.
- Hook up to the DataServices ProcessingPipeline.
Which is best?
Undoubtedly the ProcessingPipeline option is easier to understand and has less moving parts. Which makes it an ideal solution for simple scenarios.
But the ProcessingPipeline is only an option if it makes sense to allow anonymous access to the rest of website. Which is pretty unlikely unless the web application only exists to host the Data Service.
Using ProcessingPipeline.ProcessingRequest
Nevertheless the ProcessingPipeline approach is informative, and most of the code involved can be reused if you ever need to upgrade to a fully fledged HttpModule.
So how do you use the ProcessingPipeline?
Well the first step is to enable anonymous access to your site in IIS:
Next you hookup to the ProcessingPipeline.ProcessingRequest event:
public class ProductService : DataService<Context>
{
public ProductService()
{
this.ProcessingPipeline.ProcessingRequest += new EventHandler<DataServiceProcessingPipelineEventArgs>(OnRequest);
}
Then you need some code in the OnRequest event handler to do the authentication:
void OnRequest(object sender,
DataServiceProcessingPipelineEventArgs e)
{
if (!CustomAuthenticationProvider.Authenticate(HttpContext.Current))
throw new DataServiceException(401, “401 Unauthorized”);
}
In this code we call into a CustomAuthenticationProvider.Authenticate() method.
If everything is okay – and what that means depends upon the authentication scheme – the request is allowed to continue.
If not we throw a DataServiceException which ends up as a 401 Unauthorized response on the client.
Because we are hosted in IIS our Authenticate() method has access to the current Request via the HttpContext.Current.Request.
My pseudo-code, which assumes some sort of claims based security, looks like this:
public static bool Authenticate(HttpContext context)
{
if (!context.Request.Headers.AllKeys.Contains(“Authorization”))
return false;
// Remember claims based security should be only be
// used over HTTPS
if (!context.Request.IsSecureConnection)
return false;
string authHeader = context.Request.Headers[“Authorization”];
IPrincipal principal = null;
if (TryGetPrinciple(authHeader, out principal))
{
context.User = principal;
return true;
}
return false;
}
What happens in TryGetPrincipal() is completely dependent upon your auth scheme.
Because this post is about server hooks, not concrete scenarios, our TryGetPrincipal implementation is clearly NOT meant for production (!):
private static bool TryGetPrincipal(
string authHeader,
out IPrincipal principal)
{
//
// WARNING:
// our naive – easily mislead authentication scheme
// blindly trusts the caller.
// a header that looks like this:
// ADMIN username
// will result in someone being authenticated as an
// administrator with an identity of ‘username’
// i.e. not exactly secure!!!
//
var protocolParts = authHeader.Split(‘ ‘);
if (protocolParts.Length != 2)
{
principal = null;
return false;
}
else if (protocolParts[0] == “ADMIN”)
{
principal = new CustomPrincipal(
protocolParts[1],
“Administrator”, “User”
);
return true;
}
else if (protocolParts[0] == “USER”)
{
principal = new CustomPrincipal(
protocolParts[1],
“User”
);
return true;
}
else
{
principal = null;
return false;
}
}
Don’t worry though as this series progresses we will look at enabling real schemes like Custom Basic Auth, OAuthWrap, OAuth 2.0 and OpenId.
Creating a custom Principal and Identity
Strictly speaking you don’t need to set the Current.User, you could just allow or reject the request. But we want to access the User and their roles (or claims) for authorization purposes, so our TryGetPrincipal code needs an implementation of IPrincipal and IIdentity:
public class CustomPrincipal: IPrincipal
{
string[] _roles;
IIdentity _identity;
public CustomPrincipal(string name, params string[] roles)
{
this._roles = roles;
this._identity = new CustomIdentity(name);
}
public IIdentity Identity
{
get { return _identity; }
}
public bool IsInRole(string role)
{
return _roles.Contains(role);
}
}
public class CustomIdentity: IIdentity
{
string _name;
public CustomIdentity(string name)
{
this._name = name;
}
string IIdentity.AuthenticationType
{
get { return “Custom SCHEME”; }
}
bool IIdentity.IsAuthenticated
{
get { return true; }
}
string IIdentity.Name
{
get { return _name; }
}
}
Now my authorization logic only has to worry about authenticated users, and can implement fine grained access control.
For example if only Administrators can see products, we can enforce that in a QueryInterceptor like this:
[QueryInterceptor(“Products”)]
public Expression<Func<Product, bool>> OnQueryProducts()
{
var user = HttpContext.Current.User;
if (user.IsInRole(“Administrator”))
return (Product p) => true;
else
return (Product p) => false;
}
Summary
In this post you saw how to add custom authentication logic *inside* the Data Service using the ProcessingPipeline.ProcessRequest event.
Generally though when you want to integrate security across your website and your Data Service, you should put your authentication logic *under* the Data Service, in a HttpModule.
More on that next time…
Alex James
Program Manager
Microsoft
0 comments