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!

INNVTV
  • 3,155
  • 7
  • 37
  • 71

1 Answers1

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);
            }
        }
    }
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Thanks Darin, this is precisely what I needed! – INNVTV Jul 19 '12 at 06:14
  • `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? – Slauma Aug 19 '12 at 14:23
  • @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). – Darin Dimitrov Aug 19 '12 at 14:39
  • 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... – Slauma Aug 19 '12 at 15:57
  • I've opened a separate question now: http://stackoverflow.com/questions/12028604/how-can-i-safely-set-the-user-principal-in-a-custom-webapi-httpmessagehandler – Slauma Aug 19 '12 at 17:45
  • Why do you double validate `request.Headers.Authorization != null`? – Joel Aug 28 '13 at 13:52
  • @Joel, because that's a mistake. Thanks for spotting it. Answer updated. – Darin Dimitrov Aug 28 '13 at 14:35
  • 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. – Chris Arnold Jan 30 '14 at 22:04
  • 2
    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? – Shaul Zuarets Aug 04 '15 at 11:57