ASP.NET MVC 4 Web API Authentication with Membership Provider
Asked Answered
A

1

47

I have an ASP.NET MVC 4 Project using the Web API. On the controller I have set the class to require authorization using the [Authorize] attribute. For Authentication I am using the ASP.NET Membership Provider and have my Web.Config set to use "Forms" Authentication. Here is where I am stuck:

Everything is working great up until the point that I am done with testing the API and I want to secure the controller with the [Authorize] attribute so I can start testing authentication against users in my Membership Provider. So I fire up Fiddler and make the same call adding the Authorization:Basic attribute along with a username:password from my membership provider like so:

enter image description here

The response I get is 401 unauthorized and under "Auth" I get "No WWW-Authenticate Header is present." Then I realize that the API is looking for an SHA1 encoded key. So I fire up an SHA1 generator from a search and get a hash for my username:password and update my Request Header like so:

enter image description here

This does not work either and I get the same results. Also I obviously need some sort of "shared secret key" to use with the server to decode my username/password.

So my questions:

  1. How do I get this key from the server (or in this case Virtual IIS running off VS 2012).
  2. How do I use this to make Authenticated calls in Fiddler using usernames/passwords from an ASP.NET Membership Provider.
  3. How will I use this in my client application to make the same calls (C# WPF App).
  4. Is this best practive when combined with SSL on my HTTP calls? If not what is?

Thanks in advance!

Audre answered 18/7, 2012 at 5:57 Comment(0)
M
71

You could use basic authentication with SSL. On the server side we could write a custom delegating handler which will verify the credentials by querying the memebership provider that we registered, and if valid, retrieve the roles and set the current principal:

public class BasicAuthenticationMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authHeader = request.Headers.Authorization;

        if (authHeader == null)
        {
            return base.SendAsync(request, cancellationToken);
        }

        if (authHeader.Scheme != "Basic")
        {
            return base.SendAsync(request, cancellationToken);
        }

        var encodedUserPass = authHeader.Parameter.Trim();
        var userPass = Encoding.ASCII.GetString(Convert.FromBase64String(encodedUserPass));
        var parts = userPass.Split(":".ToCharArray());
        var username = parts[0];
        var password = parts[1];

        if (!Membership.ValidateUser(username, password))
        {
            return base.SendAsync(request, cancellationToken);
        }

        var identity = new GenericIdentity(username, "Basic");
        string[] roles = Roles.Provider.GetRolesForUser(username);
        var principal = new GenericPrincipal(identity, roles);
        Thread.CurrentPrincipal = principal;
        if (HttpContext.Current != null)
        {
            HttpContext.Current.User = principal;
        }

        return base.SendAsync(request, cancellationToken);
    }
}

We then register this handler in Application_Start:

GlobalConfiguration.Configuration.MessageHandlers.Add(
    new BasicAuthenticationMessageHandler()
);

Now we could have an Api controller that will be decorated with the [Authorize] attribute to ensure that only authenticated users can access its actions:

[Authorize]
public class ValuesController : ApiController
{
    public string Get()
    {
        return string.Format("Hello {0}", User.Identity.Name);
    }
}

Alright, now let's look at a sample client:

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

class Program
{
    static void Main()
    {
        // since for testing purposes I am using IIS Express
        // with an invalid SSL certificate I need to desactivate
        // the check for this certificate.
        ServicePointManager.ServerCertificateValidationCallback += 
            (sender, certificate, chain, sslPolicyErrors) => true;

        using (var client = new HttpClient())
        {
            var buffer = Encoding.ASCII.GetBytes("john:secret");
            var authHeader = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(buffer));
            client.DefaultRequestHeaders.Authorization = authHeader;
            var task = client.GetAsync("https://localhost:44300/api/values");
            if (task.Result.StatusCode == HttpStatusCode.Unauthorized)
            {
                Console.WriteLine("wrong credentials");
            }
            else
            {
                task.Result.EnsureSuccessStatusCode();
                Console.WriteLine(task.Result.Content.ReadAsAsync<string>().Result);
            }
        }
    }
}
Mcwhirter answered 18/7, 2012 at 7:29 Comment(9)
Thanks Darin, this is precisely what I needed!Audre
User.Identity.Name in the controller method is empty for me, User.Identity.IsAuthenticated is false. I can see in the debugger that Thread.CurrentPrincipal is set to the principal with correct user name and roles in the message handler. Also the [Authorization(Roles=...)] attribute is respected: If I use a user with wrong roles I get an Unauthorized response and don't reach the controller method. But User.Identity doesn't have the value I've set in the message handler. Do you have an idea what could be wrong?Festinate
@Slauma, I am unable to reproduce this. Maybe you could start a new thread explaining step by step how to reproduce the problem (starting from an empty project of course).Mcwhirter
I couldn't reproduce it myself in a fresh project. But in the meantime I found that the User principal is correct in the ApiController constructor, but wrong in the controller method. In between is a custom MediaTypeFormatter. By setting some breakpoints I see that the chain of calls is: ApiController constructor->MediaTypeFormatter->ApiController method. Somehow in the formatter the principal gets overwritten although I don't use anything security related there. If I comment out the formatter registration in global.asax, the principal stays correct. I need to continue debugging...Festinate
I've opened a separate question now: #12029104Festinate
Why do you double validate request.Headers.Authorization != null?Jungian
@Joel, because that's a mistake. Thanks for spotting it. Answer updated.Mcwhirter
This doesn't work for me :(. The handler gets called for browser requests. However, when called from 'code' the endpoint gets redirected (302) to the login page and the handler is never invoked.Radiance
Hi, the method: "Membership.ValidateUser()" is returning false. I'm assuming I need to set the membership default database connection? If so, where do I do it?Wintry

© 2022 - 2024 — McMap. All rights reserved.