Click here to Skip to main content
15,867,453 members
Articles / Web Development / ASP.NET

Various Clients and Forms Authentication

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
26 Apr 2011CPOL8 min read 41.3K   18   13
Using Forms Authentication to secure access to ASP.NET MVC endpoints and WCF services

Conceptual

Scenarios

In each of the below scenarios, forms authentication is used to secure access to the ASP.NET MVC endpoints and WCF services.

  • WPF accessing WCF services
  • WPF accessing MVC endpoints
  • Windows Phone 7 accessing WCF services
  • Desktop and mobile browsers accessing ASP.NET MVC website pages
  • jQuery accessing MVC endpoints

While I have not included the code for a Windows Phone 7 to access MVC endpoints that is a supported scenario as well.

Requirements

  • Visual Studio 2010
  • SQL Express 2008
  • Windows Phone 7 tools
    • If you don’t have these and don’t want to see this project, you can just remove the Windows Phone 7 project from the solution.

Background

I’ve been working on Stuff v2; a movie, game, and books application. Its primary use case is, "I’m at the store and don’t remember if I have a particular movie, game, or book. I need to determine if I have it; if not, then check the online price and ratings before making the purchase."

Given the varied application clients and devices, ASP.NET forms authentication seemed like the natural choice for authentication for the website, MVC3 JSON endpoints, and WCF services.

The reason I have varied client software and devices is more of a learning experience than an application requirement. I have other applications I want to write that will need to access the application from all my devices.

When I started programming the WPF client, I ran into a stone wall with respect to WPF accessing WCF services that are secured by forms authentication. This blog post is about getting over that stone wall.

Identifying the Problem Space

At the end of the day, the problem that needs solving is managing the forms authentication cookie or ticket.

Managing means that after authenticating, the client must be able to retrieve the ticket returned in the response and include it in future requests to secured resources.

As you will see, client APIs vary across scenarios not only in coding patterns but in complexity as well.

Regardless of which client is accessing resources that require forms authentication, the following steps outline the required workflow:

  • Log in
  • Cache the ticket returned in the response
  • Include the ticket in subsequent requests

Setting up Forms Authentication

When I created the ASP.NET MVC3 application, VariousClients.Web, I used the MVC3 Internet template with the Razor view engine. This template sets up forms authentication and provides a pretty good out-of-box SQL Express membership system for you.

The below snippet from the web.config shows a few required changes:

HTML
<authentication mode="Forms">
    <!-- cookieless="UseCookies" is required by non-browser clients
                to authenticate using forms authentication-->

    <!-- production applications, change to requiresSSL="true"-->
    <forms timeout="2880" 
    cookieless="UseCookies" loginUrl="~/Account/LogOn"
            requireSSL="false" />
</authentication>

Setting up the AuthenticationService

The System.Web.ApplicationServices.AuthenticationService is a built-in service that you can expose as a service endpoint on your website. This service exposes log in, log out methods for clients that access WCF endpoints requiring forms authentication. This service uses the membership provider defined in the web.config. After logging in, the service returns a ticket in the response, similar to forms authentication log in.

Adding the service is easy. First, add a folder to the root of the website named, "Services". Into that folder, add a WCF service named Authentication.svc. Delete the generated service contract and code-behind files. Next, replace the contents of the Authentication.scv file with the below code snippet.

ASP.NET
<%@ <span style="COLOR: maroon">ServiceHost Language="C#" 
   Service="System.Web.ApplicationServices.AuthenticationService" %>

Now add the following to your web.config:

XML
<system.web.extensions>
    <scripting>
        <webServices>
            <!-- for production applications, change to requiresSSL="true"-->
            <authenticationService enabled="true" requireSSL="false"/>
        </webServices>
    </scripting>
</system.web.extensions>

Rebuild your web application.

The Authentication.svc will now appear in the Add Service Reference dialog when adding service references in your client applications.

AddServiceReference

Browsers, Web pages, and MCV3 Controller Methods

Controller classes or controller action methods can be decorated with the Authorize attribute to require that client browsers or JavaScript accessing them be authenticated.

After logging in, the browser automatically manages the authentication ticket and includes it with all future requests to the website.

The below GetTime method requires authentication:

C#
using System;
using System.Web.Mvc;

namespace VariousClients.Web.Controllers {

    public class CloudDataController : Controller {

        [Authorize]
        public JsonResult GetTime() {
            return Json(DateTime.Now.ToLongDateString(), JsonRequestBehavior.AllowGet);
        }
    }
}

WPF and MVC3 Controller Methods

The following section applies equally to WPF, Windows Forms, console, and test projects.

Server-Side

I’ve added a RemoteLogOn method to the AccountController that is used by non-browser clients when logging in. The method signature and implementation is slightly different from the LogOn method.

C#
        [HttpPost]
#if (!DEBUG)
        <span style="COLOR: gray">[RequireHttps]
#endif
        public Boolean RemoteLogOn(LogOnModel model) {
            if(ModelState.IsValid) {
                if(Membership.ValidateUser(model.UserName, model.Password)) {
                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    return true;
                }
            }
            return false;
        }

CookieAwareWebClient

Remember, the purpose of the above RemoteLogOn is to get the ticket returned in the response. To simplify client programming, a CookieAwareWebClient wrapper can be used to execute WebClient requests while at the same time handling including the ticket in requests.

C#
using System;
using System.Net;

namespace VariousClients.Common.Net {

    public class CookieAwareWebClient : WebClient {

        public CookieContainer CookieContainer { get; private set; }

        public CookieAwareWebClient()
            : this(new CookieContainer()) {
        }

        public CookieAwareWebClient(CookieContainer cookieContainer) {
            if (cookieContainer == null) 
            throw new ArgumentNullException("cookieContainer");
            this.CookieContainer = cookieContainer;
        }

        protected override WebRequest GetWebRequest(Uri address) {
            if(this.CookieContainer == null) {
                throw new InvalidOperationException("CookieContainer is null");
            }
            var request = base.GetWebRequest(address);
            if (request is HttpWebRequest) {
                (request as HttpWebRequest).CookieContainer = this.CookieContainer;
            } return request;
        }
    }
}

Client-Side

C#
void btnMvcLogIn_Click(Object sender, RoutedEventArgs e) {

    _cookieJar = new CookieContainer();
    var client = new CookieAwareWebClient(_cookieJar) { Encoding = Encoding.UTF8 };

    client.UploadValuesCompleted += (s, args) => {
        this.lblMvcResult.Content =
            args.Error == null ? Encoding.UTF8.GetString(args.Result) : 
                          args.Error.Message;
    };

    var nvc = new NameValueCollection { { "UserName", Credentials.UserName },
                                        { "Password", Credentials.Password },
                                        { "RememberMe", "true" } };
    client.UploadValuesAsync(
        new Uri("http://localhost:1668/Account/RemoteLogOn"), "POST", nvc);
}

void btnMvcGetData_Click(Object sender, RoutedEventArgs e) {

    this.lblMvcResult.Content = "calling cloud service...";
    var client = new CookieAwareWebClient(_cookieJar);

    client.DownloadStringCompleted += (s, args) => {
        this.lblMvcResult.Content = 
          args.Error == null ? args.Result : args.Error.Message;
    };

    client.DownloadStringAsync(new Uri("http://localhost:1668/CloudData/GetTime"));
}

In the above code, the _cookieJar is scoped at module level. The important concept is to use the same instance of the CookieAwareWebClient for all calls because it manages the ticket for you. As an alternative, you could retrieve the CookieContainer value after logging in and pass it in the constructor when creating new CookieAwareWebClient instances.

The btnMvcGetData_Click code looks like a typical WebClient call. The CookieAwareWebClient makes calling endpoints secured by forms authentication painless.

WPF and WCF Service Methods

The following section applies equally to WPF, Windows Forms, console, and test projects.

WPF consuming WCF services secured with forms authentication requires a bit of extra code to manage the ticket because WCF does not provide a simple API to add or retrieve it when making service calls that use the generated proxies.

If you contrast the WPF and Windows Phone 7 calls to the same WCF service, you’ll see exactly what I mean. The Windows Phone 7 proxy exposes a CookieContainer object making it easy to capture or include the ticket in all service calls.

Before you can call the AuthenticationService, you’ll need to add a service reference to the Authentication service. When adding the service reference, I changed the namespace to AuthenticationService like the below image:

AddServiceReference

FormsAuthenticationAssistant

The FormsAuthenticationAssistant is a façade for WPF clients making WCF service calls secured with forms authentication. It provides automatic ticket management for making service calls.

C#
// Many thanks to Jonas Follesoe for this post:
// http://jonas.follesoe.no/2008/09/12/wcf-authentication
//         -services-silverlight-and-smelly-cookies/
// I learned how to extract the forms authentication
// cookie from the AuthenticationService and how to
// reapply it subsequent service calls.

using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace VariousClients.Common.ServiceModel {

    public class FormsAuthenticationAssistant {

        public String TicketCookie { get; private set; }

        public FormsAuthenticationAssistant() { }

        public FormsAuthenticationAssistant(String ticket) {
            if (String.IsNullOrWhiteSpace(ticket))
                throw new ArgumentNullException("ticket");
            this.TicketCookie = ticket;
        }

        public Boolean Login(Func<Boolean> method, 
                             IContextChannel serviceInnerChannel) {
            if (method == null) throw new ArgumentNullException("method");
            if (serviceInnerChannel == null)
                throw new ArgumentNullException("serviceInnerChannel");

            using (new OperationContextScope(serviceInnerChannel)) {
                if (!method()) {
                    this.TicketCookie = null;
                    return false;
                }
                var properties = OperationContext.Current.IncomingMessageProperties;
                var responseProperty =
                    (HttpResponseMessageProperty)
                     properties[HttpResponseMessageProperty.Name];
                this.TicketCookie = 
                   responseProperty.Headers[HttpResponseHeader.SetCookie];
                return true;
            }
        }

        public T Execute<T>(Func<T> method, 
                        IContextChannel serviceInnerChannel) {
            if (method == null) throw new ArgumentNullException("method");
            if (serviceInnerChannel == null)
                throw new ArgumentNullException("serviceInnerChannel");
            if (String.IsNullOrWhiteSpace(this.TicketCookie)) {
                throw new InvalidOperationException(
                    "Currently not logged in. Must Login before calling this method.");
            }

            using (new OperationContextScope(serviceInnerChannel)) {
                var requestProperty = new HttpRequestMessageProperty();
                OperationContext.Current.OutgoingMessageProperties.Add(
                    HttpRequestMessageProperty.Name, requestProperty);
                requestProperty.Headers.Add(HttpRequestHeader.Cookie, this.TicketCookie);
                return method();
            }
        }

        public void Execute(Action method, IContextChannel serviceInnerChannel) {
            if (method == null) throw new ArgumentNullException("method");
            if (serviceInnerChannel == null)
                throw new ArgumentNullException("serviceInnerChannel");
            if (String.IsNullOrWhiteSpace(this.TicketCookie)) {
                throw new InvalidOperationException(
                    "Currently not logged in. Must Login before calling this method.");
            }

            using (new OperationContextScope(serviceInnerChannel)) {
                var requestProperty = new HttpRequestMessageProperty();
                OperationContext.Current.OutgoingMessageProperties.Add(
                    HttpRequestMessageProperty.Name, requestProperty);
                requestProperty.Headers.Add(HttpRequestHeader.Cookie, this.TicketCookie);
                method();
            }
        }
    }
}

The above Login method invokes the method passed in the method argument. If the log in is successful, the ticket is extracted from the response and cached in the TicketCookie property and true is returned.

The above two Execute methods invoke the method passed in the method argument and add the TicketCookie to the request.

Your application can interface with the FormsAuthenticationAssistant in one of two ways. One is to create a single instance of the FormsAuthenticationAssistant and use that instance for all service calls. This is how the demo WPF application is written.

Another is to save the value of the TicketCookie property after a successful log in, then create a new instance of FormsAuthenticationAssistant for each call and pass the saved TicketCookie value in the constructor.

Once you’re comfortable with routing your service proxy calls through the FormsAuthenticationAssistant, you’ll notice very little difference between using this façade and calling the service proxy methods directly. Don’t forget, in real-world applications you’ll want to make these service calls or calls to the façade asynchronous so the UI thread is not blocked.

Server-Side

The PrincipalPermission attribute can be used on WCF service methods to restrict access to authenticated users. This is similar to using the Authorize attribute on the MCV3 controller action methods. Follow the three steps below:

C#
using System;
using System.Security.Permissions;
using System.ServiceModel.Activation;
using System.Threading;
using System.Web;

namespace VariousClients.Web.Services {

    //STEP 1 - required for interop with ASP.NET
    [AspNetCompatibilityRequirements(
       RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class CloudData : ICloudData {

        public CloudData() {
            //STEP 2 - pass caller principal to executing threads principal
            Thread.CurrentPrincipal = HttpContext.Current.User;
        }

        //STEP 3 - verify caller is authenticated
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public String GetTime() {
            return DateTime.Now.ToLongDateString();
        }
    }
}

Client-Side

In the below code, we will first log in against the AuthenticationService, cache the ticket, then call CloudData.GetTime.

C#
void btnServiceLogIn_Click(Object sender, RoutedEventArgs e) {
    this.lblServiceResult.Content = "calling cloud service...";
    var authenticationService = new AuthenticationServiceClient();
    if(_faa.Login(() => authenticationService.Login(
                    Credentials.UserName, Credentials.Password, String.Empty, true),
                    authenticationService.InnerChannel)) {
        this.lblServiceResult.Content = "Log in successful.";
    } else {
        this.lblServiceResult.Content = "Bummer, log in not successful.";
    }
}

void btnServiceGetData_Click(Object sender, RoutedEventArgs e) {
    this.lblServiceResult.Content = "calling cloud service...";
    var cloudDataService = new CloudDataClient();
    try {
        this.lblServiceResult.Content =
            _faa.Execute<String>(cloudDataService.GetTime, cloudDataService.InnerChannel);
    } catch(System.ServiceModel.Security.SecurityAccessDeniedException ex) {
        // three conditions could cause this
        //  1. client has never logged in
        //  2. ticket is expired
        //  3. instance of _faa was not the same instance used to log in
        // when this happens in your client code, re-authenticate to get a ticket
        this.lblServiceResult.Content = ex.Message;
    } catch(Exception ex) {
        this.lblServiceResult.Content = ex.Message;
    }
}

This above code is straightforward and very familiar to developers making service calls. The paradigm change is the forwarding of WCF service proxy calls through the FormsAuthenticationAssistant instance (_faa) which manages the ticket.

Windows Phone 7 and WCF Service Methods

Before you can call the AuthenticationService, you’ll need to add a service reference to the Authentication service. When adding the service reference, I changed the namespace to AuthenticationService like the below image:

AddServiceReference

For each service reference you add, you must also edit the ServiceReferences.ClientConfig file binding entry and set the enableHttpCookieContainer property to true as done below.

The Visual Studio tooling for adding service references chokes when the enableHttpCookieContainer property is set on a binding and displays a bizarre error message when adding or updating service references.

So… add all your service references, and then edit the ServiceReferences.ClientConfig file.

If you have to subsequently add, delete or modify a service reference, you’ll need to remove these properties, use the tooling and re-add the properties.

XML
<bindings>
    <basicHttpBinding>
        <binding name="BasicHttpBinding_AuthenticationService" 
        maxBufferSize="2147483647"
            maxReceivedMessageSize="2147483647" 
            enableHttpCookieContainer="true">
            <security mode="None" />
        </binding>
        <binding name="BasicHttpBinding_ICloudData" 
        maxBufferSize="2147483647"
            maxReceivedMessageSize="2147483647" 
            enableHttpCookieContainer="true">
            <security mode="None" />
        </binding>
    </basicHttpBinding>
</bindings>

Calling the AuthenticationService Login method is just like calling other WCF service methods with a minor exception; the CookieContainer property must be set to a new instance of the CookieContainer class prior to making the call. If you forget to populate this property, the ticket will not be returned in the response.

C#
void btnLogin_Click(Object sender, RoutedEventArgs e) {
    this.tbResult.Text = "Attempting to log in...";
    _cookieJar = new CookieContainer();
    var authenticationService = new AuthenticationServiceClient();
    authenticationService.CookieContainer = _cookieJar;
    authenticationService.LoginCompleted += (s, args) => {
        if(args.Error == null) {
            this.tbResult.Text = "Login successful.";

            // CookieJar can be persisted without an exception being thrown
            // either when tombstoned or shutdown.
            // Yes, it can be read from the below stores also.
            // If required for your application,
            // move this code to the appropriate location
            // for launching, closing, activating, and deactivating.
            //
            // uncomment to test
            // PhoneApplicationService.Current.State.Add("CookieJar", _cookieJar);
            // IsolatedStorageSettings.ApplicationSettings.Add("CookieJar", _cookieJar);

        } else {
            this.tbResult.Text = "Login failed: " + args.Error.Message;
        }
    };
    authenticationService.LoginAsync(Credentials.UserName, 
                  Credentials.Password, String.Empty, true);
}

In the above code, I’ve created a module level variable to hold the CookieContainer (_cookieJar). The same CookieContainer instance must be passed in subsequent calls to other WCF service endpoints.

With Windows Phone 7, programming core concepts like tombstoning and launching must be addressed. In the above code snippet, you’ll see a section of comments showing the CookieContainer persisted to storage. Based on your application's needs and security requirements, you’ll need to determine when and how to persist your CookieContainer so that your users will not have to repeatedly log in.

C#
void btnGetServiceData_Click(Object sender, RoutedEventArgs e) {
    this.tbResult.Text = "Calling service...";
    var cloudDataService = new CloudDataClient { CookieContainer = _cookieJar };
    cloudDataService.GetTimeCompleted += (s, args) => {
        if(args.Error == null) {
            this.tbResult.Text = args.Result;
        } else {
            if(args.Error is System.ServiceModel.Security.SecurityAccessDeniedException) {
                // three conditions could cause this
                //  1. client has never logged in
                //  2. ticket is expired
                //  3. the _cookieJar instance is not the same instance used when logging in
                // when this happens in your client code, re-authenticate to get a ticket
                this.tbResult.Text = args.Error.Message;
            } else {
                this.tbResult.Text = "Error: " + args.Error.Message;
            }
        }
    };
    cloudDataService.GetTimeAsync();
}

In the above code, the _cookieJar is assigned to the CookieContainer property. Contrast this API with the WPF API for the same call, this API is much simpler. It would be nice to have this exposed in the desktop APIs as well.

Running the Application

  • Set the VariousClients.Web project as the startup project.
  • Run the application (this will create your membership SQL Express database.)
  • When the below page is displayed, click the Register link.

RunningFirstTime

  • When completing the Registration page, you need to use the user name and password specified in the below Credentials class. This class provides the user name and password for the WPF and Windows Phone 7 projects when making service calls.

Credentials

  • After completing a registration page, you will be redirected to the page that demonstrates making a jQuery AJAX call to a secured MVC endpoint.
  • To run the WPF and Windows Phone 7 applications, set them as the startup projects.

Download

The demo solution can be downloaded from my Sky Drive here.

Close

Hope this helps when writing your own desktop, phone and web applications that use forms authentication.

Have a great day!

Just a grain of sand on the worlds beaches.

License

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



Comments and Discussions

 
GeneralMy vote of 5 Pin
Rahul Rajat Singh6-Mar-15 0:41
professionalRahul Rajat Singh6-Mar-15 0:41 
QuestionIIS comma-separated cookies breaks auth Pin
bvoltmer4-Dec-14 10:51
bvoltmer4-Dec-14 10:51 
The WPF client here works well with a single cookie. Under Forms Authentication, IIS will provide two cookies: ".ASPXAUTH" and "ASP.NET_SessionId". The format supplied by IIS differs from the format it requires. IIS’ output has cookies comma-separated with cookie attributes semicolon-separated. IIS wants the cookies supplied to it semicolon-separated and without attributes. One of the attributes IIS provides is "HttpOnly". I found that if the client didn’t process the cookie header, IIS reads the second cookie as "HttpOnly,.ASPXAUTH" instead of ".ASPXAUTH". This breaks authorization and prevents access to the WCF service.
QuestionIIS + https -> principal permission failed Pin
bvoltmer2-Dec-14 8:21
bvoltmer2-Dec-14 8:21 
QuestionRequest for principal permission failed Pin
lost.in.translation26-Jun-12 11:42
lost.in.translation26-Jun-12 11:42 
GeneralRe: Request for principal permission failed Pin
lost.in.translation26-Jun-12 12:15
lost.in.translation26-Jun-12 12:15 
QuestionThe download link is broken.... Pin
Member 392702022-May-12 1:15
Member 392702022-May-12 1:15 
AnswerRe: The download link is broken.... Pin
User 27100922-May-12 3:07
User 27100922-May-12 3:07 
QuestionCalling via JQuery Pin
groovq230-Jun-11 14:30
groovq230-Jun-11 14:30 
AnswerRe: Calling via JQuery Pin
User 2710091-Jul-11 3:00
User 2710091-Jul-11 3:00 
GeneralWhy is this blog article tagged as C++? Pin
Aoi Karasu 18-May-11 2:01
professional Aoi Karasu 18-May-11 2:01 
GeneralRe: Why is this blog article tagged as C++? Pin
User 2710091-Jul-11 2:56
User 2710091-Jul-11 2:56 
GeneralMy vote of 5 Pin
ksafford12-May-11 4:17
ksafford12-May-11 4:17 
GeneralRe: My vote of 5 Pin
User 2710091-Jul-11 2:57
User 2710091-Jul-11 2:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.