1

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?

Keith Murray
  • 635
  • 1
  • 5
  • 15
  • I have read this question several times and I am still not sure what the question is, or where the failure has occurred. Is it failing when you try to call the Web API from your web application using server-side code? What exactly is the failure? Are you receiving any errors or is it unexpected behavior? – Kevin Junghans Aug 29 '13 at 12:33
  • 1
    For starters I would not recommend calling the Web API from the server-side code. This is unnecessary network traffic. Take a look at this suggested architecture [http://stackoverflow.com/questions/14887871/creating-a-service-layer-for-my-mvc-application/14896644#14896644] which will let you access your application domain directly. You can still handle authorization without the Web API call using a custom ClaimsAuthorizationManager. – Kevin Junghans Aug 29 '13 at 12:38
  • Sorry about not putting my specific error in there. I spent quite a bit of time trying to put in all the details and missed one of the most important. It is indeed failing to authorize with the Web API when called from the server side code. – Keith Murray Aug 29 '13 at 17:11
  • @KevinJunghans I appreciate the comment about unnecessary network traffic and the link. It would probably make sense to something like that. Right now though I want to get this working as I will need to authenticate and call this Web API from .NET client applications as well. I'm testing from within my web app at first because it seems convenient. – Keith Murray Aug 29 '13 at 17:14
  • 1
    Since you are making a Web API call from server-side code you will have to include the cookie in the http request. Using basic authentication will not work since you will not know the password for the user. The custom Authorize attribute you are using performs both authentication and authorization. This is a lot of overhead for just making a method call to your application domain. Again, I recommend you call your application domain directly (instead of going through a Web API) and use a custom ClaimsAuthorizationManager to perform authorization only. User has already been authenticated. – Kevin Junghans Aug 29 '13 at 17:25
  • Will you be using only web clients? A web browser will automatically handle the cookies for you. If the clients are not web-based then you can use basic authentication, assuming the client is configured with the correct user name and password combination. A none-web client would not use User.Identity.Name to get a valid user name since the principal identity is on the server, not the client. – Kevin Junghans Aug 29 '13 at 17:30
  • I do include the cookie, as shown in the code above. In the BasicAuthorizeAttribute.cs class implemented in my Web API (from the link above about mixing simplemembership and basic authentication), I can call `FormsAuthentication.GetAuthCookie( username, false )` and the cookie is returned. However `WebSecurity.IsAuthenticated` returns false. Good practice or not, this should work, shouldn't it? Once I get it working I will move on to doing this right as you recommend. – Keith Murray Aug 29 '13 at 17:35
  • I will be using non-web clients as well, using .NET. I was planning to call a 'login' URL to authenticate and obtain the cookie, saving the cookie and then using it for further authentication when calling the API. Seems reasonable. – Keith Murray Aug 29 '13 at 17:46

1 Answers1

1

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!

Keith Murray
  • 635
  • 1
  • 5
  • 15