Search code examples
asp.net-mvc-4authenticationazuresimplemembership

Sharing ASP.NET simplemembership authentication across web roles and client applications


I've been trying to figure this out for a while now, reading a lot of blogs, MSDN documentation, sample code and other stackoverflow questions and have yet to get this to work.

Here is my scenario:

I am using Windows Azure to host two web roles. One is my MVC4 web API, the other is my MVC4 web app which uses the web API. I also have a number of client applications using .NET that will access the web API.

So my main components are:

  • Web API
  • Web App
  • .NET Client

I want to use forms authentication that is 'hosted' in the Web App. I am using the built in simplemembership authentication mechanism and it works great. I can create and log in to accounts in the Web App.

Now I also want to use these same accounts to authenticate the Web API, both from the Web App and any .NET client apps.

I've read numerous ways to do this, the simplest appearing to be using Basic Authentication on the Web API. Currently I am working with this code as it appears to solve my exact problem: Mixing Forms Authentication, Basic Authentication, and SimpleMembership

I can't get this to work. I log in successfully to my Web App (127.0.0.1:81) and when I try to call a Web API that requires authentication (127.0.0.1:8081/api/values for example) the call fails with a 401 (Unauthorized) response. In stepping through the code, WebSecurity.IsAuthenticated returns false. WebSecurity.Initialized returns true.

I've implemented this code and am trying to call my Web API from my Web App (after logging in) with the following code:

using ( var handler = new HttpClientHandler() )
{
    var cookie = FormsAuthentication.GetAuthCookie( User.Identity.Name, false );
    handler.CookieContainer.Add( new Cookie( cookie.Name, cookie.Value, cookie.Path, cookie.Domain ) );

    using ( var client = new HttpClient() )
    {
        //client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        //  "Basic",
        //  Convert.ToBase64String( System.Text.ASCIIEncoding.ASCII.GetBytes(
        //  string.Format( "{0}:{1}", User.Identity.Name, "123456" ) ) ) );
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Cookie",
            Convert.ToBase64String( System.Text.ASCIIEncoding.ASCII.GetBytes( User.Identity.Name ) ) );

        string response = await client.GetStringAsync( "http://127.0.0.1:8080/api/values" );

        ViewBag.Values = response;
    }
}

As you can see, I've tried both using the cookie as well as the username/password. Obviously I want to use the cookie, but at this point if anything works it will be a good step!

My ValuesController in my Web API is properly decorated:

// GET api/values
[BasicAuthorize]
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

In my Global.asax.cs in my Web API, I am initializing SimpleMembership:

// initialize our SimpleMembership connection
try
{
    WebSecurity.InitializeDatabaseConnection( "AzureConnection", "User", "Id", "Email", autoCreateTables: false );
}
catch ( Exception ex )
{
    throw new InvalidOperationException( "The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex );
}

This succeeds and WebSecurity later says that it is initialized so I guess this part is all working properly.

My config files have matching authentication settings as required per MSDN.

Here is the API config:

<authentication mode="Forms">
<forms protection="All" path="/" domain="127.0.0.1" enableCrossAppRedirects="true" timeout="2880" />
</authentication>
<machineKey decryption="AES" decryptionKey="***" validation="SHA1" validationKey="***" />

Here is the Web App config:

<authentication mode="Forms">
<forms loginUrl="~/Account/Login" protection="All" path="/" domain="127.0.0.1" enableCrossAppRedirects="true" timeout="2880" />
</authentication>
<machineKey decryption="AES" decryptionKey="***" validation="SHA1" validationKey="***" />

Note, I am trying this locally (hence the 127.0.0.1 domain), but referencing a database hosted on Azure.

I haven't got to trying any of this from a .NET client application since I can't even get it working between web roles. For the client app, ideally I would make a web call, passing in username/password, retrieve the cookie, and then use the cookie for further web API requests.

I'd like to get what I have working as it seems pretty simple and meets my requirements.

I have not yet tried other solutions such as Thinktecture as it has way more features than I need and it doesn't seem necessary.

What am I missing?


Solution

  • Well, this is embarrassing. My main problem was a simple code error. Here is the correct code. Tell me you can spot the difference from the code in my question.

    using ( var handler = new HttpClientHandler() )
    {
        var cookie = FormsAuthentication.GetAuthCookie( User.Identity.Name, false );
        handler.CookieContainer.Add( new Cookie( cookie.Name, cookie.Value, cookie.Path, cookie.Domain ) );
    
        using ( var client = new HttpClient( handler ) )
    ...
    }
    

    Once that was fixed, I started getting 403 Forbidden errors. So I tracked that down and made a small change to the BasicAuthorizeAttribute class to properly support the [BasicAuthorize] attribute when no role is specified.

    Here is the modified code:

    private bool isAuthorized( string username )
    {
        // if there are no roles, we're good!
        if ( this.Roles == "" )
            return true;
    
        bool authorized = false;
    
        var roles = (SimpleRoleProvider)System.Web.Security.Roles.Provider;
        authorized = roles.IsUserInRole( username, this.Roles );
        return authorized;
    }
    

    With that change basic authentication by passing in the forms cookie works!

    Now to get non-web client apps working and then refactor the Web App as recommended.

    I hope this helps someone in the future!