Leveraging Forms Authentication in Web Api

Web Api technology is not like ASP.NET MVC, even though the two are often used together.  Although most people probably host Web Api inside IIS, and I do as well, Web Api does not take dependency on System.Web or IIS.  This allows you to self-host Web Api 2.0 application in your own process, such as Windows Service for example.  As a result, of course, Web Api has no idea of Forms Authentication, defined in System.Web assembly.  You can of course, use custom headers to secure Web Api against unauthorized calls.  I found this approach a bit tedious because you have to come up with your own way to inject headers into requests.  If you are writing smart client app, that is probably OK.  However, if you are writing browser based app, such as ASP.NET MVC app, I think, cookie is much easier to implement and use because the responsibility to transmit the cookie falls onto browser.  Browser also knows how to expire and persist cookies between sessions, which is a bonus.  So far I convinced myself to use cookies for authentication and authorization.  Now I need to come up with a way to implement this in Web Api.  I am going to reference System.Web and use Forms Authentication.  Yes, I am taking dependency that Web Api avoided, but I do not see a giant downside to that.  The reason is that I do not take dependency on HttpContext.Current, so I should be able to self-host.  To convince myself I can just host my service inside HttpService that comes with WebApi host NuGet package.  unit test project.  Now I can add this to my base class for tests:

protected HttpMessageInvoker PrepareServer()
{

            var server = new HttpServer(CreateHttpConfiguration());
            return new HttpMessageInvoker(server);
}

protected HttpConfiguration CreateHttpConfiguration()
{
            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
            return config;
}

I can also use SelfHostConfiguration and HttpSelfHostServer, but I want to simulate full cycle of sending and receiving messages. I will blog more about full cycle testing in later posts.

 

Now, off to the next step.  I know I want to use a cookie, but I want to ensure that before an action is executed, my cookie is valid.  I am going to write an Action Filter for this.  Note that this is Web Api action filter, not MVC, which has the class with the same name, but not quite the same signature.  There are three ways you can inject those filters into your Web Api app.  You can decorate a controller or a controller method (action) or just add the filter to global filter collection:

GlobalConfiguration.Configuration.Filters.Add(new AuthorizationFilterAttribute());

You can decide which way you want to do it.  I decided to have two base controller classes – one for public actions, such as login.  The other controller base class is for authenticated users only.  This way I just decorate the authorized base class controller with my attribute.  Here is the code for the filter, simplified a bit. AuthorizedController is my base controller class, protected with this new attribute.

using System;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Security;
using Newtonsoft.Json;

namespace App.WebApi.Core.Filters
{
    public class AuthorizationFilterAttribute : ActionFilterAttribute
    {
        private SecurityHeader _securityHeader;
        private SecurityCookieHeader _cookieHeader;
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var controller = (AuthorizedController)actionContext.ControllerContext.Controller;
                var header =
                    actionContext.Request.Headers.GetCookies(FormsAuthentication.FormsCookieName);
                if (header != null && header.Count > 0)
                {
                    var cookie =
                        header.First()
                            .Cookies.FirstOrDefault(one => one.Name == FormsAuthentication.FormsCookieName);

                    if (cookie != null && cookie.Value != null)
                    {
                        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);

                        if (ticket != null)
                        {
                            _cookieHeader =
                                JsonConvert.DeserializeObject<SecurityCookieHeader>(ticket.UserData);
                            _securityHeader = controller.CacheProvider.Get<SecurityHeader>(_cookieHeader.Email);
                            if (_securityHeader == null)
                            {
                                SetSecurityHeader();
                            }
                            if (!SecurityHelper.Login(_securityHeader).Identity.IsAuthenticated)
                            {
                                throw new SecurityException("Unable to login");
                            }
                            controller.SecurityHeader = _securityHeader;
                            base.OnActionExecuting(actionContext);
                        }
                        else
                        {
                            throw new SecurityException("Unable to login");
                        }
                    }
                    else
                    {
                        throw new SecurityException("Unable to login");
                    }
                }
                else
                {
                    throw new SecurityException("Unable to login");
                }

            }
            catch (SecurityException exception)
            {
                var controller = (AuthorizedController)actionContext.ControllerContext.Controller;
                controller.ExceptionHandler.HandleException(exception);
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(
                        new ExecutionResult<bool?>
                    {
                        ErrorMessage = "Invalid login.",
                        Result = null,
                        Success = false
                    }))
                };
            }

        }

        private void SetSecurityHeader()
        {
            // cache the user data for speed and set it on a property of AuthorizedController to identify the user inside the controller action
        }
    }
}

As you can see I just want to make sure I am getting an authenticated request and I want to set user data on my controller, in case my action need to perform authorization.  I am storing only user ID in the cookie to keep it small, and cache the rest of the data on the server for speed.  Login controller is a public controller and Login method is the other portion of authentication cycle.

        public HttpResponseMessage Login(Login login)
        {
            SecurityHeader header = null;
            SecurityCookieHeader cookieHeader = null;
            var executionResult = Execute(() =>
            {
                var result = _masterRepository.Login(login.UserEmail, login.Password);
                if (result.IsSuccess)
                {

                    header = new SecurityHeader
                    {
                        CreatedOn = DateTime.Now,
                        TenantId = result.TenantId,
                        UserId = result.UserId,
                        ConnectionString = result.ConnectionString,
                        Email = login.UserEmail
                    };
                    cookieHeader = new SecurityCookieHeader
                    {
                        TenantId = header.TenantId,
                        UserId = header.UserId,
                        Email = header.Email
                    };
                    return result.Name;
                }
                return string.Empty;
            });
            var message = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(JsonConvert.SerializeObject(executionResult))
            };
            if (executionResult.Success && cookieHeader != null)
            {
                var ticket = new FormsAuthenticationTicket(
                    1, login.UserEmail, DateTime.Now, DateTime.Now.AddDays(1), false, JsonConvert.SerializeObject(cookieHeader));

                string encryptedTicket = FormsAuthentication.Encrypt(ticket);

                var cookie = new CookieHeaderValue(FormsAuthentication.FormsCookieName, encryptedTicket);
                cookie.Path = FormsAuthentication.FormsCookiePath;
                message.Headers.AddCookies(new[] { cookie });
                CacheProvider.Set(login.UserEmail, header);
            }
            return message;
        }

In my case I create 24 hour cookie, but it is not persisted in the browser by default.  So, after user shuts down browser, the user will have to login again, even if 24 hours has not expired. You can persist the cookie as well, you just need to add expiration date to CookieHeaderValue

Another neat thing about this approach is that I can share the cookie with MVC app as well, writing a similar filter, but I only need to handle unauthenticated request.  You can always add authorization in your case.

using System.Web.Mvc;
using System.Web.Security;
using Newtonsoft.Json;
using App.Common.Security;

namespace App.Web.Core.Filters
{
    public class AuthorizationFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
              
            var cookie =
                filterContext.RequestContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
            if (cookie == null)
            {
                filterContext.Result = new HttpUnauthorizedResult();
            }
            else
            {
                if (cookie.Value != null)
                {
                    FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);

                    if (ticket != null)
                    {
                        try
                        {
                            SecurityCookieHeader cookieHeader = JsonConvert.DeserializeObject<SecurityCookieHeader>(ticket.UserData);
                            if (!string.IsNullOrEmpty(cookieHeader.Email))
                            {
                                base.OnActionExecuting(filterContext);
                            }
                            else
                            {
                                filterContext.Result = new HttpUnauthorizedResult();
                            }
                        }
                        catch
                        {
                            filterContext.Result = new HttpUnauthorizedResult();
                        }
                       
                    }
                    else
                    {
                        filterContext.Result = new HttpUnauthorizedResult();
                    }
                }
                else
                {
                    filterContext.Result = new HttpUnauthorizedResult();
                }
            }
            base.OnActionExecuting(filterContext);
        }
    }
}

I hope I was able to explain why I like this approach to secure Web Api.

Enjoy.

Thanks.

5 Comments

  1. Thanks,

    I had to add a new Web Api project to an existing project already using FormAuthentication cookie with another SOAP API.

    I finally chose to use a custom Header with the ticket instead of a cookie

Leave a Reply

Your email address will not be published. Required fields are marked *