OData and Authentication – Part 7 – Forms Authentication
Our goal in this post is to re-use the Forms Authentication already in a website to secure a new Data Service.
To bootstrap this we need a website that uses Forms Auth.
Turns out the MVC Music Store Sample is perfect for our purposes because:
- It uses Forms Authentication. For example when you purchase an album.
- It has a Entity Framework model that is clearly separated into two types of entities:
- Those that anyone should be able to browse (Albums, Artists, Genres).
- Those that are more sensitive (Orders, OrderDetails, Carts).
The rest of this post assumes you’ve downloaded and installed the MVC Music Store sample.
Enabling Forms Authentication:
The MVC Music Store sample already has Forms Authentication enabled in the web.config like this:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
With this in place any services we add to this application will also be protected.
Adding a Music Data Service:
If you double click the StoreDB.edmx file inside the Models folder you’ll see something like this:
This is want we want to expose, so the first step is to click ‘Add New Item’ and then select new WCF Data Service:
Next modify your MusicStoreService to look like this:
public class MusicStoreService : DataService<MusicStoreEntities>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Carts", EntitySetRights.None);
config.SetEntitySetAccessRule("OrderDetails", EntitySetRights.ReadSingle);
config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
config.SetEntitySetPageSize("*", 50);
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V2;
}
}
The PageSize is there to enforce Server Driven Paging, which is an OData best practice, we don’t like to show samples that skip this… :)
Then the three EntitySetAccessRules in turn:
- Hide the Carts entity set – our service shouldn’t expose it.
- Allow OrderDetails to be retrieved by key, but not queried arbitrarily.
- Allow all other sets to be queried by not modified – in this case we want the service to be read-only.
Next we need to secure our ‘sensitive data’, which means making sure only appropriate people can see Orders and OrderDetails, by adding two QueryInterceptors to our MusicStoreService:
[QueryInterceptor("Orders")]
public Expression<Func<Order, bool>> OrdersFilter()
{
if (!HttpContext.Current.Request.IsAuthenticated)
return (Order o) => false;
var username = HttpContext.Current.User.Identity.Name;
if (username == "Administrator")
return (Order o) => true;
else
return (Order o) => o.Username == username;
}
[QueryInterceptor("OrderDetails")]
public Expression<Func<OrderDetail, bool>> OrdersFilter()
{
if (!HttpContext.Current.Request.IsAuthenticated)
return (OrderDetail od) => false;
var username = HttpContext.Current.User.Identity.Name;
if (username == "Administrator")
return (OrderDetail od) => true;
else
return (OrderDetail od) => od.Order.Username == username;
}
These interceptors filter out all Orders and OrderDetails if the request is unauthenticated.
They allow the administrator to see all Orders and OrderDetails, but everyone else can only see Orders / OrderDetails that they created.
That’s it - our service is ready to go.
NOTE: if you have a read-write service and you want to authorize updates you need ChangeInterceptors.
Trying it out in the Browser:
The easiest way to logon is to add something to your cart and buy it:
Which prompts you to logon or register:
The first time through you’ll need to register, which will also log you on, and then once you are logged on you’ll need to retry checking out.
This has the added advantage of testing our security. Because at the end of the checkout process you will be logged in as the user you just registered, meaning if you browse to your Data Service’s Orders feed you should see the order you just created:
If however you logoff, or restart the browser, and try again you’ll see an empty feed like this:
Perfect. Our query interceptors are working as intended.
This all works because Forms Authentication is essentially just a HttpModule, which sits under our Data Service, that relies on the browser (or client) passing around a cookie once it has logged on.
By the time the request gets to the DataService the HttpContext.Current.Request.User is set.
Which in turn means our query interceptors can enforce our custom Authorization logic.
Enabling Active Clients:
In authentication terms a browser is a passive client, that’s because basically it does what it is told, a server can redirect it to a logon page which can redirect it back again if successful, it can tell it to include a cookie in each request and so on...
Often however it is active clients – things like custom applications and generic data browsers – that want to access the OData Service.
How do they authenticate?
They could mimic the browser, by responding to redirects and programmatically posting the logon form to acquire the cookie. But no wants to re-implement html form handling just to logon.
Thankfully there is a much easier way.
You can enable an standard authentication endpoint, by adding this to your web.config:
<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true" requireSSL="false"/>
</webServices>
</scripting>
</system.web.extensions>
The endpoint (Authentication_JSON_AppService.axd) makes it much easier to logon programmatically.
Connecting from an Active Client:
Now that we’ve enabled the authentication endpoint, lets see how we use it. Essentially for forms authentication to work the DataServiceContext must include a valid cookie with every request.
A cookie is just a http header and, as we saw in part 3, it is very easy to add a custom header with every request.
Using Client Application Services:
But before we get down to setting cookies, in some scenarios there is an even easy way: using Client Application Services. These services are not available in the .NET Client Profile (or Silverlight) so you may need to change your Target Framework to use them:
Once you’ve done that you enable Client Application Services like this:
NOTE: the Authentication Services Location should be set to the root of the website that has Authentication Services enabled.
Next you add a reference to System.Web to gain access to System.Web.Security.Membership.
Once you’ve done this you simply need to logon once:
System.Web.Security.Membership.ValidateUser("Alex", "password");
This logs on and stores the resulting cookie on the current thread.
Next, assuming you already have a Service Reference to your Data Service – see this to learn how – you can extend your custom DataServiceContext, in our example called MusicStoreEntities, to automatically send the cookie with each request:
public partial class MusicStoreEntities
{
partial void OnContextCreated()
{
this.SendingRequest +=
new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
}
void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
((HttpWebRequest)e.Request).CookieContainer =
((ClientFormsIdentity)Thread.CurrentPrincipal.Identity).AuthenticationCookies;
}
}
This works by adding the partial OnContextCreated method, which is called in the MusicStoreEntities constructor, and hooking up to the SendingRequest event, to set the cookie for each request.
That’s it, pretty easy.
Manually setting the Cookie:
If however using Client Application Services is not an option – for example you’re in Silverlight or you can only use the Client Profile – you will have to manually get and set the cookie.
To do this change the example above to look like this instead:
public partial class MusicStoreEntities
{
partial void OnContextCreated()
{
this.SendingRequest +=
new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
}
public void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
e.RequestHeaders.Add("Cookie", GetCookie("Alex", "password"));
}
string _cookie;
string GetCookie(string userName, string password)
{
if (_cookie == null)
{
string loginUri = string.Format("{0}/{1}/{2}",
"https://localhost:1397",
"Authentication_JSON_AppService.axd",
"Login");
WebRequest request = HttpWebRequest.Create(loginUri);
request.Method = "POST";
request.ContentType = "application/json";
string authBody = String.Format(
"{{ \"userName\": \"{0}\", \"password\": \"{1}\", \"createPersistentCookie\":false}}",
userName,
password);
request.ContentLength = authBody.Length;
StreamWriter w = new StreamWriter(request.GetRequestStream());
w.Write(authBody);
w.Close();
WebResponse res = request.GetResponse();
if (res.Headers["Set-Cookie"] != null)
{
_cookie = res.Headers["Set-Cookie"];
}
else
{
throw new SecurityException("Invalid username and password");
}
}
return _cookie;
}
}
This code is admittedly a little more involved. But it you break it down it all makes sense.
The code adds the cookie to the headers whenever a request is issued.
The hardest part is actually acquiring the cookie. The GetCookie() method checks whether we have a cookie, if not it creates a request to the Authentication endpoint, passing the username and password in a JSON body.
If authentication is successful the response will include a ‘Set-Cookie’ header, that contains the cookie.
Summary:
We’ve just walked through using Forms Authentication with an OData service.
That included: integrating security with an existing website, enabling both browser and active clients – based on DataServiceContext – and authenticating from any .NET client.
Next up we’ll start looking at things like OAuth and OAuthWrap…
Alex James
Program Manager
Microsoft.
Comments
Anonymous
August 03, 2010
The comment has been removedAnonymous
August 29, 2010
This is excellent, but what if you're only using WCF Service and not WCF Data Service - could you please explain how to send the authentication cookie in this case?Anonymous
November 13, 2010
Very informative article. Thank You!!Anonymous
December 15, 2010
void service_SendingRequest(object sender, System.Data.Services.Client.SendingRequestEventArgs e) { HttpCookie c = System.Web.HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; if (c != null) { e.RequestHeaders.Add("Cookie", string.Format("{0}={1}", c.Name, c.Value)); } }Anonymous
January 26, 2011
Thanks for this - worked like a charm!Anonymous
February 22, 2011
Excellent article, one question though,Quote:"assuming you already have a Service Reference to your Data Service – see this to learn how – you can extend your custom DataServiceContext"In my case service is exposed to external users who can build client applications; how can we allow developers to add reference to the service with authentication turned on, because when we try to add reference it redirects to the login page instead of the downloading the metadata.Anonymous
February 22, 2011
Excellent article, one question though,Quote:"assuming you already have a Service Reference to your Data Service – see this to learn how – you can extend your custom DataServiceContext"In my case service is exposed to external users who can build client applications; how can we allow developers to add reference to the service with authentication turned on, because when we try to add reference it redirects to the login page instead of the downloading the metadata.Anonymous
May 01, 2011
Let's say if the client is a windows phone 7 application, how do you send cookie from a windows phone OData client back to WCF? SendingRequestEventArgs only has RequetHeader and it won't work although you manully add form authentication cookie to it. ThanksAnonymous
December 04, 2011
It worked fine on WP7 client when I send value like, e.RequestHeaders["Cookie"] = string.Format("{0}={1}", name, value) where name and value are cookie name, value.Anonymous
June 24, 2012
Im kinda puzzled how can I Implement this authentication for WP7 client. I switched this to asynch manner with this example msdn.microsoft.com/.../system.net.httpwebrequest.begingetrequeststream.aspx but In the end in the responce i never see that cookie. Its never there and headers are all the same even with bad and good authentication data. Can anybody help me?Anonymous
July 16, 2012
Hi, can you send sample code for this project. That helps me a lot.