Mixing Forms Authentication, Basic Authentication, and SimpleMembership

ASP.NET MVC 4 introduced us to ASP.NET Web API, which makes it much easier to develop RESTful API's.  These API's can be consumed by your MVC web application or by external sources. This prompts many to ask, how can I create a single API that is secure to the outside yet incorporate the security methods that are fundamental to ASP.NET forms authentication.  I was interested in a solution myself so I tried out some concepts and I think I found a pretty good solution.

My solution incorporates the use of basic authentication along with with the typical forms authentication.  It also uses the SimpleMembership that is part of MVC 4 Internet applications. To follow along with this tutorial you will need to start with my introduction on customizing and seeding SimpleMembership.  I will use the membership information seeded in this example for testing the application.

Basic authentication puts the encoded credentials in the header of the HTTP request. In order to capture this information on the server side, perform the authentication, and authorize the user I created a custom AuthorizeAttribute.  Note that this AuthorizeAttribute is specific for Web API's and is in the System.Web.Http namespace and should not be confused with the one used for MVC controllers, which is in the System.Web.Mvc namespace.  Here is what the custom AuthorizeAttribute looks like.

   [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method, Inherited = true,
       AllowMultiple = true)]
    public class BasicAuthorizeAttribute : AuthorizeAttribute
    {

        private enum AuthType { basic, cookie };

        private string DecodeFrom64(string encodedData)
        {

            byte[] encodedDataAsBytes
                = System.Convert.FromBase64String(encodedData);
            string returnValue =
               System.Text.Encoding.ASCII.GetString(encodedDataAsBytes);

            return returnValue;
        }

        private bool GetUserNameAndPassword(HttpActionContext actionContext, out string username, out string password, out AuthType authType)
        {
            authType = AuthType.basic;
            bool gotIt = false;
            username = string.Empty;
            password = string.Empty;
            IEnumerable headerVals;
            if (actionContext.Request.Headers.TryGetValues("Authorization", out headerVals))
            {
                try
                {
                    string authHeader = headerVals.FirstOrDefault();
                    char[] delims = { ' ' };
                    string[] authHeaderTokens = authHeader.Split(new char[] { ' ' });
                    if (authHeaderTokens[0].Contains("Basic"))
                    {
                        string decodedStr = DecodeFrom64(authHeaderTokens[1]);
                        string[] unpw = decodedStr.Split(new char[] { ':' });
                        username = unpw[0];
                        password = unpw[1];
                    }
                    else
                    {
                        if (authHeaderTokens.Length > 1)
                            username = DecodeFrom64(authHeaderTokens[1]);
                        authType = AuthType.cookie;
                    }

                    gotIt = true;
                }
                catch { gotIt = false; }
            }

            return gotIt;
        }

        private bool Authenticate(HttpActionContext actionContext, out string username)
        {
            bool isAuthenticated = false;
            username = string.Empty;
            string password;
            AuthType authenticationType;

            if (GetUserNameAndPassword(actionContext, out username, out password, out authenticationType))
            {

                if (authenticationType == AuthType.basic)
                {
                    if (WebSecurity.Login(username, password, true))
                    {
                        isAuthenticated = true;
                    }
                    else
                    {
                        WebSecurity.Logout();
                    }
                }
                else //authType == cookie
                {
                    if (WebSecurity.IsAuthenticated )
                        isAuthenticated = true;

                    username = WebSecurity.CurrentUserName;
                }
            }
            else
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
            }

            return isAuthenticated;
        }

        private bool isAuthorized(string username)
        {
            bool authorized = false;

            var roles = (SimpleRoleProvider)System.Web.Security.Roles.Provider;
            authorized = roles.IsUserInRole(username, this.Roles);
            return authorized;
        }

        public override void OnAuthorization(HttpActionContext actionContext)
        {
            string username;
 
            if (Authenticate(actionContext, out username))
            {
                  if (!isAuthorized(username))
                      actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
            }
            else
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
            }
        }
    }

First lets look at the overridden OnAuthorization Method. This method is required for a customized AuthorizeAttribute and is the main entry into this filter.  This method calls the Authenticate method to get the credentials from the HTTP header and authenticate the user.  When we dive into this method in more detail you will see that also allows for the use of cookies so that our web client is not required to store credentials and send them on each request.  If the user is authenticated then we check if the user is authorized or not.  Notice that if they are not authenticated we return an unauthorized status code to the client and if the they are not authorized we send a forbidden status code. This allows the client to know if the failure was on authentication or authorization and it can act accordingly.

The Authenticate method first gets the authorization information from the header. Notice that the header can indicate that we are using basic authentication or cookies.  If we are using basic authentication we use the decoded user name and password from the header and try to authenticate the user using the WebSecurity class that is part of SimpleMembership.  If we are using cookies then we just make sure the current user is authenticated.  The magic for getting the authorization information out of the header is in the method GetUserNameAndPassword.

This is probably a good time to remind everyone that basic authentication alone is not good enough security. You still must use HTTPS/SSL in your web application to make it secure.

So lets see how this would work be creating a simple Web API controller. Our controller looks like this.

    public class TestBasicController : ApiController
    {
        [BasicAuthorize (Roles="Admin")]
        public string Get()
        {
            return "Authorized & Authenticated to use API.";
        }
    }

We have decorated the Get method with our custom AuthorizeAttribute and specified the role to authorize on to be "Admin". We have this role already seeded in our database from the previous exercise.  Now lets create a web page to test this with.

@{
    ViewBag.Title = "Test Basic Authentication";
}

<h2>Test Basic Authentication</h2>
<p>
    If you leave the username and password blank it will attempt to make the Web API request 
    using cookies. If you enter the username and password it will use basic authentication.

</p>
User Name: <input type="text" id="username" /> <br />
Password:  <input type="password" id="password" /> <br />
<input type="button" id="test-button" value="Press this button to call Web API" /> <br />
<label id="api-results"></label>

@section scripts{

    @Scripts.Render("~/Scripts/jquery.base64.js")

<script id="scriptInit" type="text/javascript">
    $(document).ready(function () {

        $("#test-button").click(runTest);

        function getAuthorizationHeader(username, password) {
            var authType;

            if (password == "") {
                authType = "Cookie " + $.base64.encode(username);
            }
            else {
                var up = $.base64.encode(username + ":" + password);
                authType = "Basic " + up;
            };
            return authType;
        };

        function ajaxSuccessHandler(data) {
            $("#api-results").text(data);
        };

        function ajaxErrHandler(jqXHR, textStatus, errorThrown) {
            $("#api-results").text(errorThrown + " : " + textStatus);
        }


        function runTest() {

            var username = $("#username").val();
            var password = $("#password").val();
            $("#api-results").text("");

            $.ajax({
                url: "api/TestBasic",
                type: "GET",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("Authorization", getAuthorizationHeader(username, password));
                },
                success: ajaxSuccessHandler,
                error: ajaxErrHandler
            });
        };

    });
</script>

}

This view has input for a user name and password. If they are left blank it will attempt to use cookies. If they are filled in it will attempt basic authentication.  To make the the request to the Web API we made in the previous step we will  use  the JQuery ajax method.  In order to insert information in the header we will use the beforeSend event to fill in the authorization information.  The function getAuthorizationHeader is where the user name and password are encoded for basic authentication or it includes a string indicating that  cookies should be used.  For encoding the JQuery base64 plugin is used, which you can get here.

Now lets test out this solution. If I start up the app and go to the page we just setup for testing, and just press the test button I will get back an Unauthorized error message as shown in the following figure.


 This is because the user is not logged in yet. Now lets put in some credentials and try this again but with a user that is not in the "Admin" role.  Now if we press the test button we get a Forbidden error because they were authenticated but not authorized for this Web API call.


Now lets log in with credentials for a user that is in the "Admin" role.  Now you will see the results of our Web API displayed when we hit the test button.


Now if we remove the user name and password and press the test button again it will successfully call the Web API using cookies and forms authentication.

This solution works well for authentication/authorization of Web API methods that can be used by MVC views or by external clients. This can also be easily extend so that you could use AJAX to handle the user login. Try it out and let me know what you think.

You can get the source code for this project here.

Comments

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0