Using Membership Provider to Protect WC Data Service

I blogged previously about securing services with forms authentication.  In this blog post I would like to describe a shortcut to creating a secure service using membership provider infrastructure. 

To start with, we need to create a provider.  It is pretty easy step, as you can inherit System.Web.Security.MembershipProvider and override any methods you need to.  I am sticking to basic case, and all I would like to do is authenticate against a database for example.  To do this, I just override ValidateUser method, and leave other methods blank (or with default code generated by studio).

        public override bool ValidateUser(string username, string password)
        {
            var returnValue = false;
            using (var context = new ChinookEntities())
            {
                var user = context.Employees.FirstOrDefault(one => one.Email == username);
                if (user != null)
                {
                    if (ValidatePassword(user, password))
                    {
                        returnValue = true;
                    }
                }
            }
            return returnValue;
        }

Now, we can just update web.config to enable forms authentication and setup our new membership provider.

    <authentication mode="Forms">
      <forms cookieless="UseCookies" timeout="2800" slidingExpiration="true" loginUrl="/"></forms>
    </authentication>
    <authorization>
      <deny users="?" />
    </authorization>
    <membership defaultProvider="default">
      <providers>
        <clear/>
        <add name="default"
             type="MyWCFDataService.DefaultMemerbshipProvider, MyWCFDataService, Version=1.0.0.0, Culture=neutral"
             serviceUri="http://localhost/MyWCFDataService/Authentication_JSON_AppService.axd"/>
      </providers>
    </membership>
  </system.web>
  <location path="Authentication_JSON_AppService.axd">
    <system.web>
      <authorization>
        <allow users="?" />
      </authorization>
    </system.web>
  </location>
  <system.web.extensions>
    <scripting>
      <webServices>
        <authenticationService enabled="true" requireSSL="false" />
      </webServices>
    </scripting>
  </system.web.extensions>

There are a few things to notice here.  My membership provider is configured with assembly qualified name.  I also setup default end point with built-in authentication.  It must be named Authentication_JSON_AppService.axd to work.  I also exclude this end point from forms authentication by using location tag and allowing unauthenticated users.

The next step is to enable form authentication in IIS for my application.  If you skip this step, your entire effort is for not.

Now, let’s take a look at how to use this from WinRT application.  I am into extension methods these days, so I added new extension method Login to my wcf proxy.  I can add proxy code to my WInRT application by simply adding service reference and pointing to my WCF Service svc file.  Again, you will need to disable forms authentication in web.config, since proxy generation cannot work with protected end points.

Let’s take a look at the extension method.

        public static async Task<Cookie> Login(this DataServiceContext context, string userName, string password)
        {
            HttpClientHandler handler = new HttpClientHandler();
            handler.CookieContainer = new CookieContainer();
            var client = new HttpClient(handler);
            var uri = new Uri(context.BaseUri.ToString().Replace(@"MyDataService.svc/", @"Authentication_JSON_AppService.axd/Login"));

            string authBody = String.Format(
                "{{ "userName": "{0}", "password": "{1}", "createPersistentCookie":false}}",
                userName,
                password);

            var responce = await client.PostAsync(uri, new StringContent(authBody, new UTF8Encoding(), "application/json"));
            var cookies = handler.CookieContainer.GetCookies(uri);
            if (cookies.Count == 0)
            {
                return null;
            }
            return cookies[".ASPXAUTH"];

        }

I need to call this method first thing, in my case from my view model

        private async void Load()
        {
            IsBusy = true;
            _cookie = await _chinookEntities.Login("andrew@chinookcorp.com", "1");
            if (_cookie == null)
            {
                MessageDialog dialog = new MessageDialog("Login failed");
                dialog.ShowAsync();
            }
            else
            {
                // run your code    
            }
            
            IsBusy = false;
        }

You can use await when calling this method.  It is written using Task<Cookie> as return value.  Once I have the cookie I can inject it into subseqnet requests using SendingRequest event

        public MainViewModel()
        {
            _myEntities.SendingRequest += OnSendingRequest;
        }

        void OnSendingRequest(object sender, SendingRequestEventArgs e)
        {
            e.RequestHeaders["Cookie"] = _cookie.ToString();
        }

In summary, if you do not need custom authentication and you only need basic password validation, membership provider and built-in authentication services will offer you this functionality with less code.

Enjoy.

9 Comments

  1. Hi Sergey,

    I tried your approach and it works well with IIS and DB on premise.

    After I migrate the service and the DB to Azure Cloud Services.
    If I call the login method from the client, I get no cookie.
    In the application eventlog of the Azure machine it get the error message:

    Event code: 3005
    Event message: An unhandled exception has occurred.
    Event time: 4/4/2013 3:15:42 PM
    Event time (UTC): 4/4/2013 3:15:42 PM
    Event ID: e9eecf0d17b74832a76f93d07d71a7db
    Event sequence: 2
    Event occurrence: 1
    Event detail code: 0

    Application information:
    Application domain: /LM/W3SVC/1273337584/ROOT-1-130095621418513750
    Trust level: Full
    Application Virtual Path: /
    Application Path: E:sitesroot
    Machine name: RD00155D57226D

    Process information:
    Process ID: 3600
    Process name: w3wp.exe
    Account name: NT AUTHORITYNETWORK SERVICE

    Exception information:
    Exception type: ArgumentException
    Exception message: Unknown web method Login/.
    Parameter name: methodName
    at System.Web.Script.Services.RestHandler.CreateHandler(WebServiceData webServiceData, String methodName)
    at System.Web.Script.Services.ScriptHandlerFactory.GetHandler(HttpContext context, String requestType, String url, String pathTranslated)
    at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
    at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

    Request information:
    Request URL: http://538c04a574994db1b4e9d680dbc58238.cloudapp.net/Authentication_JSON_AppService.axd/Login/
    Request path: /Authentication_JSON_AppService.axd/Login/
    User host address: 88.153.72.88
    User:
    Is authenticated: False
    Authentication Type:
    Thread account name: NT AUTHORITYNETWORK SERVICE

    Thread information:
    Thread ID: 6
    Thread account name: NT AUTHORITYNETWORK SERVICE
    Is impersonating: False
    Stack trace: at System.Web.Script.Services.RestHandler.CreateHandler(WebServiceData webServiceData, String methodName)
    at System.Web.Script.Services.ScriptHandlerFactory.GetHandler(HttpContext context, String requestType, String url, String pathTranslated)
    at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
    at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

    thanks for any help.

    Frank

  2. I did not change the URL:

    var uri = new Uri(TMSDataService.BaseUri.ToString().Replace(@”TMSDataService.svc”, @”Authentication_JSON_AppService.axd/Login”));

    And it is not an MVC.

    I think IIS on Azure machine cannot handle the login URL, but I don´t know why.
    I compared all settings between on premise IIS and Azure IIS, but I can´t find a difference.

  3. in my first comment, I copied the error of the event log.

    In the meantime I tried the sample code from your Las Vegas session (WCF Data Services). There you uncomment this code:

    var uri = new Uri(context.BaseUri.ToString().Replace(@”ChinooDataService.svc/”, @”LoginService.svc/Login?userName=” + userName + “&password=” + password));

    I wanted to try this approach, but unfortunatly there is noch code file for LoginService.svc.
    Do you still have this code?

Leave a Reply

Your email address will not be published. Required fields are marked *