Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Securing your Web Services Using Forms Authentication

4.81/5 (11 votes)
19 Dec 2007CPOL8 min read 1   901  
Add a security level to your Web Services using the ASP.NET Forms Authentication.

Screenshot -

Introduction

Web Services are great. They are flexible, and they cross language boundaries. However, in most cases, securing the functionality a Web Service exposes is very useful.

If you wanted to add a custom authentication mechanism to your Web-Service, you could:

  • Request the user credentials for every web method (very ugly).
  • Write some sort of validation code that must be called first which would supply a token. This token would be sent along with every request, and the Web-Service would have to validate the token. This method requires consumers to know your API pretty well, and would require you to implement some sort of State machine on the web to enforce token expiration, etc.
  • The cleanest way (to my mind any way) would be to require the consumer (browser, etc.) to validate prior to accessing the Web Service. This is the method I will discuss here.

The simplest way to add an authentication level to a Web-Service on IIS is to simply deny anonymous access to the directory the Web-Service is located under, and let IIS authenticate requests against the Active Directory.

But what happens if you want to control the access to your Web-Services in a more flexible way, not having to add users to the Active Directory? This is where things get tricky.

ASP.NET introduced a cool authentication mechanism called FormsAuthentication, and it comes all bundled with controls to create and manage users against a database (usually SQL Server). This allows a very flexible level of authentication, and keeps all the authorization local to the Application scope.

The only problem with it is that it isn't a true HTTP authentication mechanism, and so Web-Service consumers can't really validate against it.

This is where HttpModules come into play, and we will see how we are going to manage the authentication against the FormsAuthentication Membership Provider.

HttpModules

ASP.NET provides a mechanism for plugging code into the guts of the HTTP request pipeline. I won't go into too much detail about how it works, except that you create a class that implements IHttpModule and you register your module in the web.config file. From then on, ASP.NET notifies your module on every request made to it.

So what we are going to do is: build an IHttpModule that will intercept any request made to our Web-Service and add HTTP authentication to the request.

WWW-Authenticate

In an HTTP call, if the server requires logon credentials, it adds the WWW-Authenticate header to the response. Upon receiving the response, the client is expected to prompt the user for the logon credentials, which are then sent back to the same address of the original call inside the WWW-Authenticate header.

There are a few methods for sending credentials in HTTP, but we will be discussing only two of them:

  • Basic - uses simple text posts
  • Digest - (supported by IE 5.5+) uses an MD5 hash

Basic Authentication

Basic Login

The simplest and obviously easiest to implement is the basic. In this method, the browser encodes the response into a Base64 string containing the username:password. It is then up to us to decode the string and validate the request.

As you probably realized, there isn't much encryption here, and the credentials can be decoded fairly simply over a normal connection. However, this is not the case over HTTP (SSL), and so this method is not commonly used unless you have an SSL connection.

Digest Authentication

Digest Login

Without going into too much depth, in Digest mode, the server sends a unique nonce to the client, who in turn must encrypt the user credentials along with and based on the supplied nonce using the MD5 hash algorithm. The resulting hash is sent back to the server, and it is responsible for accepting the credentials.

This method is far more secure over basic connections; however, it is currently only supported by IE.

Using the Code

Prior to adding the code provided, you must set up your web application to use FormsAuthentication. It does not matter what provider you use as long as it runs under the FormsAuthentication Membership Provider.

The code provided includes a class implementing IHttpModule, called ServiceIdentityManager.

Extract the class and add it to your app_code directory (alternatively, create a new project and add it to it; compile and reference the project from your web site).

Registering the Module

The first thing we need to do is register the module with ASP.NET. This is done by adding the following XML snippet into the web.config file under the <system.web> section:

XML
<httpModules>
    <add name="IdentityManager" type="ServiceIdentityManager"/>
</httpModules>

This tells ASP.NET that it should load our module located in the ServiceIdentityManager<code> class.

When to Validate

Once a module has been registered, it gets initialized every time a new call is made to the server. It is at that point that we wire up our events. Since we are dealing with the authentication process, we will wire up the AuthenticateRequest event from the HttpApplication sent to us in the Init function.

From this point on, we will be called whenever a call is requested to authenticate by the server. However, this is global to our entire web application, and we probably don't want a logon form for every request, so we must check to see if we need to run our authentication code at this point.

Validating

In the code supplied, I call the function NeedsAuthentication whenever a request is made, to test and see if the request is being made to a file under the Services directory. You should override this method and add your own logic to it.

The code supplied has a property called AuthenticationMode which specifies what type of authentication (Basic or Digest) the module should use. It also supplies a property by the name of TimeoutMinutes which specifies the number of minutes a security ticket is valid for. Finally, a property named Domain specifies the name of the authentication request that will be presented to the client.

When a request we need to authenticate is made to the server, the code checks for a WWW-Authenticate header in the request. If the header is not present, it issues a response with the desired header based on the AuthenticationMode set.

C#
void context_AuthenticateRequest(object sender, EventArgs e)
{
    HttpApplication httpApplication = (HttpApplication)sender;
    //Check if we need to authenticate this request
    if (!NeedsAuthentication(httpApplication))
        return;

    //Check if an authorization ticket is present
    if(string.IsNullOrEmpty(httpApplication.Request.Headers["Authorization"]))
        requestAuthentication(httpApplication);//Request authorization
    else
    {
        string sToken = httpApplication.Request.Headers["Authorization"];
        //Read the token and validate base on its type
        if (sToken.StartsWith("Basic", StringComparison.CurrentCultureIgnoreCase))
            validateBasicAuthentication(httpApplication, sToken);
        else if (sToken.StartsWith("Digest", StringComparison.CurrentCultureIgnoreCase))
            validateDigestAuthentication(httpApplication, sToken);
        else
            requestAuthentication(httpApplication);
            //We can't understand this token, request one we will
    }
}

private void requestAuthentication(HttpApplication httpApplication)
{
    if (AuthenticationMode == AuthenticationModes.Digest)
        requestDigestAuthentication(httpApplication);
    else
        requestBasicAuthentication(httpApplication);
}

Authentication

The client is prompted and replies with credentials. These are decoded and verified against the FormsAuthentication Membership Provider.

In the case of Basic authentication, the password is supplied in clear text, and so we use it to call:

C#
Membership.ValidateUser(userName,password);

and see if the credentials are good.

In the case of Digest authentication, the issue is a bit trickier. In the validateDigestAuthentication method, we create a new hash based on the username sent and the password we extract from the Membership Provider. We check to see if this hash matches the hash sent by the client, and if it does, we consider it validated.

Note: In order for Digest authentication to work, the module must be able to get the user's password from the Membership Provider. See MSDN articles on how to set this up.

Once we have authenticated our user, we need to set the User principle of the HttpContext to a valid FormsAuthentication object.

This is done by creating a new RolePrinciple object. The constructor expects an Identity object, which we supply in the form of a FormsIdentity created using a new FormsAuthenticationTicket based on the given username. This is then set to the User Principal of the HttpContext for further use.

C#
private void setPrinciple(HttpApplication httpApplication, string userName)
{
    //Create a FormsAuthenticationTicket for our roles principle
    RolePrincipal rPrince = new RolePrincipal(new FormsIdentity(
        new FormsAuthenticationTicket(userName, false, TimeoutMinutes)));

    httpApplication.Context.User = rPrince;
}

Clash of the Modules

Up to now, things have been pretty straightforward; however, now we hit a tricky part.

Since we are authenticating against the FormsAuthentication Provider, the site must be set to use this authentication. However, when we issue our WWW-Authenticate request to the client, the response will not reach our module. Instead, it will be intercepted by the FormsAuthentication Module which will try to validate the response and basically screw up our code.

So how do we get rid of the FormsAuthentication module from doing this? The simplest way is to disable it. This is done by adding the following XML snippet into the web.config file under the <system.web> section:

XML
<httpModules>
    <remove name="FormsAuthentication" />
    <add name="IdentityManager" type="ServiceIdentityManager"/> 
</httpModules>

But if we remove the FormsAuthentication module, then the rest of our site cannot use FormsAuthentication. Bummer.

In an ideal world, what we would do is remove the FormsAuthentication module from the list of modules, add our module to the list, and then add the FormsAuthentication back in. Simple you say? Well, I wish it was. Unfortunately, this will not do the trick; for some reason, the FormsAuthentication module will still be called before our module.

And so the workaround (and I admit it to be a bit of a hack) is to remove the FormsAuthentication module, add our own module, and then add the FormsAuthenticationModule back with a new name.

XML
<httpModules>
    <remove name="FormsAuthentication" />
    <add name="IdentityManager" type="ServiceIdentityManager"/> 
    <add name="FormsAuthenticationOld" 
      type="System.Web.Security.FormsAuthenticationModule"/> 
</httpModules>

And that should solve the issue.

What is Left?

Once we implement the code above, any request made to a resource located under the Services directory will be required to send login credentials. If this is done through a web browser, the browser will prompt the user for a username/password. If you are trying to connect your Web-Service code, then simply set the Credentials property of your Web Service to a new System.Net.NetworkCredential with the username and password.

Contributions

Based in part on the article True ASP.NET Digest Authentication by Peter A. Bromberg.

History

None yet...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)