Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Authenticating with ADO.NET Data Services

Blogs

Mike Taulty's Blog

Elsewhere

Archives

One of the questions that people ask when they've seen ADO.NET Data Services and its facilities for surfacing your data over REST is;

So, how do I secure this?

There's a number of aspects to securing these systems - off the top of my head we might want some aspects of;

  1. Authentication.
  2. Authorisation or access control to the operations on the data.
  3. Privacy or encryption of the traffic flowing between client and service.
  4. Auditing operations that have been performed by the service.
  5. Some element of non-repudiability (e.g. user X cannot deny that they deleted record Y).


Thinking about authentication - whenever I've been asked "So how do I authenticate?" I've tended to just fall back to a fairly stock answer of "Well, this is all just HTTP and WCF so we just go ahead and use the traditional mechanisms that we've got for authenticating those kinds of things".

However, I thought it'd be good to try and explore those options here. What can we actually do?

Let's start by building a simple ADO.NET Data Services service and a client for it.

File New WebSite

image

Delete the ASPX page Visual Studio gives you

image

Add a new ADO.NET Data Service

image

Add a new Entity Framework Data Source to expose (in my case) my Northwind Database

image 

image

And expose that through Data Services.

public class Service : DataService<NorthwindEntities>
{
  public static void InitializeService(IDataServiceConfiguration config)
  {
    // NB: Not for production.
    config.SetEntitySetAccessRule("*", EntitySetRights.All);
  }
}

That gets us a service that we can build a client against. Essentially, I can add a new project into my solution such as a console application;

image

I can use datasvcutil.exe to generate some proxy code for me;

image

and then I can add that code into my console application project, add a reference to System.Data.Services.Client to make it compile and then write client code such as;

static void Main(string[] args)
  {
    NorthwindEntities proxy = new NorthwindEntities(
      new Uri("http://localhost/SecureSite/Service.svc"));

    var query =
      from c in proxy.Customers
      where c.Country == "UK"
      select c;

    foreach (Customers c in query)
    {
      Console.WriteLine("Customer {0} lives in the UK", c.CustomerID);
    }
  }

Now that we have a client and a service and the client talks to the service quite nicely I can start to think about breaking that by forcing the client to authenticate in order to call the service.

What are my options to enforce authentication here?

Firstly, we can ask IIS to do the authentication ( basic, digest, Ntlm, Windows, Certificate ).

The "problem" with this for me is that whilst IIS authentication is pluggable, the credential management isn't. The built-in authentication modules in IIS ( apart from ASP.NET membership ) always want to validate your credentials as though they were Windows credentials and that's a real shame because I'd imagine that isn't the key scenario 90+% of the time.

With IIS 7 you do get a pluggable pipeline but the credential validation still isn't pluggable so if you want to, for instance, validate your credentials against your own store then you end up writing something like what Dominick wrote here which does custom basic authentication for you in IIS 7.

So...IIS 7 provides a step forward but, for me, it's still not granular enough to be that useful in this area I'm afraid.

The sad thing is that if I host an HTTP-based service outside of IIS with WCF then WCF has a whole host of options for authentication and they're all pluggable in that I can just drop in my own credential management class and do the validation of the credentials there.

However, when you host WCF inside of IIS that's no longer the case and a lot of the flexibility around authentication in WCF goes away - for instance if you want to do basic authentication you need to;

  1. Switch it on in WCF. Great. That's pluggable for credential management.
  2. Switch it on in IIS. Ah. Now I just lost my pluggability of credential management.

With all that said, IIS 7 does provide a module that does ASP.NET membership-based authentication so that would be a possibility.

Alternatively, we could let the request flow through IIS without authentication and then apply ASP.NET authentication outside of IIS.

This approach works "better" in some ways because it would work with both IIS 6 and IIS 7. However, there is another caveat to aply - by default, a request for a .SVC file will not touch the ASP.NET pipeline so applying ASP.NET forms authentication to it won't do anything unless we switch WCF to use the ASP.NET interoperability mode.

If you're still reading this, does it seem to you like there's a need for simplification in all this stuff? It certainly does to me - all I want to do is get the request authenticated and I'm into a tangled-web trying to unpick the real options available to me.

So...what to choose?

I figured that I'd leave IIS with "anonymous" and use ASP.NET authentication within the bounds of my application as that'll work on any version of IIS.

I can restructure my website a little so it looks like;

image

and now I've got my Service.svc file hidden in the Secure folder. I can then edit that local web.config to say;

 <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>
  </system.web>
  <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
  </system.serviceModel>

and I can make sure that the web.config in the folder above has its authentication set to Forms and that I have a login.aspx page for it to redirect to ( as you can see in the picture ).

That's all great in that if I open up a browser and call https://localhost/SecureSite/Secure/Service.svc then I'll get redirected to login.aspx and if I authenticate myself then I get to my Service.svc file which is fine for a browser but what do I do about my .NET client? I need it to authenticate via ASP.NET, get the right cookies and then pass them on to the service whenever it makes a call.

How do I do that?

One mechanism would be to use the Client Application Services support which was added in .NET Framework V3.5 which is reasonable because ADO.NET Data Services sits on top of Framework V3.5 so, by definition, we already have that version.

This allows services like membership, roles, profile to be called as web services rather than just providing the backend data storage for some web pages.

I can switch this on by visiting my main web.config;

 <system.web.extensions>
    <scripting>
      <webServices>
        <authenticationService enabled="true" requireSSL="true"/>
      </webServices>
    </scripting>
  </system.web.extensions>

and with that switched on I can now visit my client application and switch on the same support from there ( i.e. the ability to call these services ). This is from the properties dialog for the project in Visual Studio;

image

Now, if I add a reference to my System.Data.Web assembly from my client application then I can try and log in using these services with code such as;

    if (Membership.ValidateUser("test", "Password!"))
    {

 

and that will call across to those services, check the username and password with ASP.NET membership and then provide that back to the client.

That's cool in that the client has authenticated itself to the server but I now need to ensure that when I make an ADO.NET Data Services call using the generated proxy class I'm passing the right cookies across on that call so that ASP.NET authentication allows me in to the service. That is - there's no point authenticating with Membership.ValidateUser if I can't then harvest those cookies and pass them on subsequent requests.

One mechanism ( and the only one I've found so far ) is to hook the SendingRequest event on the proxy class. For example;

  static void Main(string[] args)
  {
    // Sign in to ASP.NET membership from the client using Client App Services.
    if (Membership.ValidateUser("test", "Password!"))
    {
      NorthwindEntities proxy = new NorthwindEntities(
        new Uri("https://localhost/SecureSite/Secure/Service.svc"));

      proxy.SendingRequest += BeforeSendingRequest;

      var query =
        from c in proxy.Customers
        where c.Country == "UK"
        select c;

      foreach (Customers c in query)
      {
        Console.WriteLine("Customer {0} lives in the UK", c.CustomerID);
      }
    }
  }
  static void BeforeSendingRequest(object sender, SendingRequestEventArgs e)
  {
    // Grab the identity that ASP.NET membership set up for us
    ClientFormsIdentity aspNetIdentity = 
      Thread.CurrentPrincipal.Identity as ClientFormsIdentity;

    // Grab the web request
    HttpWebRequest wr = e.Request as HttpWebRequest;

    // Copy the cookies
    if (aspNetIdentity != null)
    {
      wr.CookieContainer = aspNetIdentity.AuthenticationCookies;
    }
  }

And I am "in business" :-) I have my server-side code doing authentication based on ASP.NET membership, I have my client side code using that same membership system to authenticate its requests into that service and I have an entirely pluggable credential validation mechanism for the back-end.

Happy.

Now, what if I wanted to do this from an AJAX client? :-) There's a couple of ways in which I can see me doing it. One is the easy way and one is the slightly-harder-way.

I can build a page that doesn't offer the "call service" functionality until you are already logged in via ASP.NET in which case it's all pretty easy. Something like the following where we have a login view that just hides the "Get Data" functionality until you're logged in;

<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server">
            <Scripts>
                <asp:ScriptReference Path="~/DataService.js" />
            </Scripts>
        </asp:ScriptManager>
        <asp:LoginView runat="server">
            <AnonymousTemplate>
                <asp:Login ID="Login1" runat="server">
                </asp:Login>
            </AnonymousTemplate>
            <LoggedInTemplate>
                <input type="button" value="Get Data" onclick="GetData()" />
                <div id="MyResults" />
            </LoggedInTemplate>
        </asp:LoginView>
    </div>
    </form>
</body>

Notice that I'm bringing in the script library for ADO.NET Data Service AJAX client which is up at CodePlex and I only offer the "Get Data" button at the point where you're already authenticated. For reference, the Javascript looks like this ( note that the 2nd function was largely taken from a quickstart );

        function GetData()
        {
            var service = new Sys.Data.DataService("Secure/Service.svc");
            service.query("Customers?$filter=Country eq 'UK'", onSuccess);
        }
        function onSuccess(result)
        {
            var tableString = new Sys.StringBuilder("<table>");

            var firstRowOutput = false;

            for (i = 0; i < result.length; i++)
            {
                var row = result[i];

                if (!firstRowOutput)
                {
                    // Display the header row.
                    tableString.append("<tr>");
                    for (key in row)
                    {
                        if (key != "__metadata")
                        {
                            tableString.append("<th>");
                            tableString.append(key);
                            tableString.append("</th>");
                        }
                    }
                }
                tableString.append("<tr>");

                firstRowOutput = true;

                // Display the data.
                tableString.append("<tr>");
                for (key in row)
                {
                    if (key != "__metadata")
                    {
                        tableString.append("<td>");
                        tableString.append(row[key]);
                        tableString.append("</td>");
                    }
                }
                tableString.append("</tr>");
            }
            tableString.append("</table>");
            $get("MyResults").innerHTML = tableString.toString();
        }  

The alternative approach would be to do the authentication from Javascript ourselves - there's a handy script library for that too so I can write a page something like;

<body onload="Loaded()">
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server">
            <Scripts>
                <asp:ScriptReference Path="~/DataService.js" />
            </Scripts>
        </asp:ScriptManager>
        <div>
        <input type="text" value="Username goes here" id="name" />
        </div>        
        <div>
        <input type="text" value="Password goes here" id="password" />
        </div>                
        <input type="button" value="Login" onclick="Login()" />
        <input type="button" value="Get Data" onclick="GetData()" />
        <input type="button" value="Logout" onclick="Logout()" />
        <div id="MyResults" />
    </div>
    </form>
</body>

and then have script;

        function Loaded()
        {
            Sys.Services.AuthenticationService.set_defaultFailedCallback(
                onFailedLogin);
        }
        function Login()
        {
            if (!Sys.Services.AuthenticationService.get_isLoggedIn())
            {
                Sys.Services.AuthenticationService.login(
                    $get("name").value,
                    $get("password").value,
                    false);
            }
        }
        function Logout()
        {
            if (Sys.Services.AuthenticationService.get_isLoggedIn())
            {
                Sys.Services.AuthenticationService.logout();
            }
        }
        function GetData()
        {
            var service = new Sys.Data.DataService("Secure/Service.svc");
            service.query("Customers?$filter=Country eq 'UK'", onSuccess,
                onFailure);
        }
        function onFailedLogin(error, context, methodName)
        {
            alert("Failed to log in, sorry");
        }
        function onFailure()
        {
            alert("Failed to get data, sorry");
        }

And that seems to largely do what I'd want it to do.

Of course, the next question is what to do about authorisation?  but that's another story.


Posted Tue, May 27 2008 7:22 AM by mtaulty

Comments

Project Astoria Team Blog wrote Securing Data Services
on Tue, May 27 2008 9:58 PM
We have received a lot of questions lately about how to authenticate calls to an ADO.NET Data Service.
Christopher Steen wrote Link Listing - May 27, 2008
on Tue, May 27 2008 11:46 PM
Announcements  [ANN] Dimecasts.net is Alive and Kicking [Via: Derik Whittaker ] ASP.NET  Create MessageBox...
Christopher Steen wrote Link Listing - May 27, 2008
on Tue, May 27 2008 11:46 PM
Link Listing - May 27, 2008
Reflective Perspective - Chris Alcock » The Morning Brew #102 wrote Reflective Perspective - Chris Alcock &raquo; The Morning Brew #102
on Tue, May 27 2008 11:57 PM
Jason Haley wrote Interesting Finds: May 28, 2008
on Wed, May 28 2008 7:14 AM
Mike Taulty's Blog wrote ADO.NET Data Services - Batching in Action
on Thu, May 29 2008 12:06 PM
I did a talk on Data Services at DevDays, Amsterdam last week and so I had to take a rather speedy look...
Mike Taulty's Blog wrote ADO.NET Data Services - Concurrency in Action
on Fri, May 30 2008 6:59 AM
Just like for batching, there's a great explanation of how concurrency looks in ADO.NET Data Services...
Weekly Links: ASP.NET MVC, .NET, ADO.NET Data Services, Silverlight, WPF… | Code-Inside Blog International wrote Weekly Links: ASP.NET MVC, .NET, ADO.NET Data Services, Silverlight, WPF&#8230; | Code-Inside Blog International
on Mon, Jun 2 2008 1:33 PM
Mike Taulty's Blog wrote Authorising with ADO.NET Data Services
on Tue, Jun 3 2008 1:23 AM
Once we've got a request authenticated the next thing we might want to consider is authorising those...
new windows based form control in net framework wrote new windows based form control in net framework
on Thu, Jun 12 2008 2:57 AM
Hot Topics wrote Mike Taulty beats on ADO.NET Data Services new features
on Fri, Jun 13 2008 6:27 AM
Rather than just listing what&amp;#39;s new, Mike tries them all out! Yay Mike. First he tackles Authentication
which control called asp function wrote which control called asp function
on Sun, Jun 22 2008 9:38 PM
visual basic browser object wrote visual basic browser object
on Sun, Jun 22 2008 11:40 PM
求索者 wrote Ado.net data service资料
on Sat, Jul 5 2008 2:00 AM
一、资源链接: 二、知识文章链接:
onclick display image wrote onclick display image
on Sat, Aug 2 2008 6:33 PM
Группа Microsoft по работе с компаниями-разработчиками wrote Аутентификация в ADO.Net Data Services
on Mon, Sep 15 2008 7:00 AM
Начинаем публикацию ответов на вопросы прошедшего 9 сентября в Москве. Как организован процесс аутентификации