Common authentication/authorization between .NET4.0 and .NET4.5 web applications

ASP.NET Identity is a big step forward and we should profit from its features, such as: two-step authentication, support for OpenId providers, stronger password hashing and claims usage. One of its requirements is .NET4.5 which might be a blocker if you have in your farm legacy Windows 2003 R2 servers still hosting some of your MVC4 (.NET4.0) applications. In this post I would like to show you how you may implement common authentication and authorization mechanisms between them and your new ASP.NET MVC5 (and .NET4.5) applications deployed on newer servers. I assume that your apps have a common domain and thus are able to share cookies.

Back in MVC4 times you probably were using forms authentication and membership roles to authorize users trying to call actions on the controllers. ASP.NET MVC5 still supports this way of securing web applications so we could achieve our goal by enabling forms/membership settings in web.config. I’m not a big fan of this solution as it won’t allow us to use more secure and feature-rich security model introduced in a new version of the framework. What I’m proposing is to use ASP.NET Identity with the Owin security pipeline in new applications and slightly modified forms authentication in older apps. Authorization should be based on claims. Our sample solution will include two applications: IdentityAuth – the MVC5 application and MembershipAuth – a legacy .NET4.0 application.

ASP.NET Identity application (IdentityAuth)

It’s a slightly modified template of the default ASP.NET MVC5 application. We will enable CookieAuthenticationMiddleware to persist user authentication data between requests:

namespace IdentityAuth
{
    public partial class Startup
    {
        // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            // Enable the application to use a cookie to store information for the signed in user
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                CookieSecure = CookieSecureOption.Never,
                Provider = new CookieAuthenticationProvider { }
            });
        }
    }
}

AccountController has only Login, Index and Logout actions defined. The Login action accepts only two accounts: test and admin (normally you would use an instance of the UserManager class to validate user accounts). Additionally test account has a special usertype claim added, which we will use in the authorization logic:

[HttpPost]
public ActionResult Login(LoginModel model)
{
    if (!String.Equals(model.Login, "test", StringComparison.Ordinal) && !String.Equals(model.Login, "admin", StringComparison.Ordinal) ||
        !String.Equals(model.Password, "1234", StringComparison.Ordinal)) {
        return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
    }

    var identity = new GenericIdentity(model.Login, "ApplicationCookie");
    var claims = new Claim[0];
    if (model.Login.Equals("test", StringComparison.Ordinal))
    {
        claims = new[] { new Claim("urn:usertype", "king") };
    }
    var claimsIdentity = new ClaimsIdentity(identity, claims);

    AuthenticationManager.SignIn(new AuthenticationProperties() { }, claimsIdentity);
    SetFormsAuthCookie(claimsIdentity);

    return RedirectToAction("Index");
}

Next to the usual AuthenticationManager.SignIn (which authenticates user in Owin-based apps) we also call SetFormsAuthCookie. This is a method which will set a forms cookie compatible with our legacy application:

UPDATE 2014.09.16: to make the auth cookie smaller I replaced the previous serialization code with more concise one

private void SetFormsAuthCookie(ClaimsIdentity identity) {
    // we need to serialize claims to string then create an auth ticket
    var cookie = FormsAuthentication.GetAuthCookie(identity.Name, false);
    var authTicket = FormsAuthentication.Decrypt(cookie.Value);
    Debug.Assert(authTicket != null, "authTicket != null");
    authTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name,
        authTicket.IssueDate, authTicket.Expiration,
        authTicket.IsPersistent,
        ExtractUserData(identity),
        authTicket.CookiePath);
    cookie.Value = FormsAuthentication.Encrypt(authTicket);
    // and place it in authorization cookie
    response.SetCookie(cookie);
}

private static String ExtractUserData(ClaimsIdentity identity)
{
    var buffer = new StringBuilder();
    var sw = new StringWriter(buffer);
    using (var jsonWriter = new JsonTextWriter(sw))
    {
        jsonWriter.WriteStartObject();
        foreach (var c in identity.Claims)
        {
            if (claims.Contains(c.Type))
            {
                jsonWriter.WritePropertyName(c.Type);
                jsonWriter.WriteValue(c.Value);
            }
        }
        jsonWriter.WriteEndObject();
    }

    return buffer.ToString();
}

Notice that in the user data section of the authentication ticket we store serialized user claims. Logout action is really simple:

public ActionResult Logout()
{
    AuthenticationManager.SignOut();
    FormsAuthentication.SignOut();

    return RedirectToAction("Login");
}

Last part of the IdentityAuth application that requires some explanation is the configuration file, especially system.web section:

  <system.web>
    <machineKey compatibilityMode="Framework20SP2" validationKey="a4c44e321ad34e783fbcc8dd58d469577097e0cef52beba39a36dc11996e06d2d8603f2155975bc22fd9367c4d66f7ff80101ad5a3339fad002d0aaadf5f6bdb" decryptionKey="31ce2f55ebf54100519d55ad62e9d93ffec98ccd8c7fcea2b6f8f1ff5a7db86c" validation="HMACSHA256" decryption="AES" />
    <compilation debug="true" targetFramework="4.5"/>
    <httpRuntime targetFramework="4.5"/>
    <customErrors mode="Off" />

    <authentication mode="None">
      <forms loginUrl="~/Account/Login" name="testauth" timeout="2880" ticketCompatibilityMode="Framework40" enableCrossAppRedirects="false" />
    </authentication>
  </system.web>

We set the authentication mode to None as we are using the Owin authentication middleware, but at the same time we configure forms authentication – these settings must be the same as in our legacy application. Notice also that machineKey has the compatibilityMode set to Framework20SP2.

Forms/Membership application (MembershipAuth)

Let’s now focus on the .NET4.0 application which needs to understand the authentication context we’ve just configured. We will start from examining system.web section of the web.config file:

  <system.web>
    <machineKey compatibilityMode="Framework20SP2" validationKey="a4c44e321ad34e783fbcc8dd58d469577097e0cef52beba39a36dc11996e06d2d8603f2155975bc22fd9367c4d66f7ff80101ad5a3339fad002d0aaadf5f6bdb" decryptionKey="31ce2f55ebf54100519d55ad62e9d93ffec98ccd8c7fcea2b6f8f1ff5a7db86c" validation="HMACSHA256" decryption="AES" />
    <httpRuntime />
    <compilation debug="true" targetFramework="4.0" />
    <authentication mode="Forms">
      <forms loginUrl="~/Account/Login" timeout="2880" name="testauth" enableCrossAppRedirects="false" />
    </authentication>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <add name="ClaimsFormsAuthentication" type="MembershipAuth.HttpModules.ClaimsFormsAuthenticationModule" />
    </modules>
  </system.webServer>

Notice that the machineKey and forms sections are exactly the same as in the IdentityAuth application. Additionally we have authentication mode set to Forms. In order to use claims identity we need to implement a custom ClaimsFormsAuthenticationModule:

UPDATE 2014.09.16: to make the auth cookie smaller I replaced the previous serialization code with more concise one

namespace MembershipAuth.HttpModules
{
    public class ClaimsFormsAuthenticationModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += context_PostAuthenticateRequest;
        }

        void context_PostAuthenticateRequest(object sender, EventArgs e)
        {
            var user = HttpContext.Current.User;
            if (user != null && user.Identity.IsAuthenticated && user.Identity is FormsIdentity)
            {
                var formsIdentity = (FormsIdentity)user.Identity;
                // user is authenticated - we will transform his identity
                var claimsPrincipal = new ClaimsPrincipal(user);
                var claimsIdentity = (ClaimsIdentity)claimsPrincipal.Identity;

                var userData = formsIdentity.Ticket.UserData;
                if (!String.IsNullOrEmpty(userData))
                {
                    var reader = new JsonTextReader(new StringReader(userData));
                    while (reader.Read())
                    {
                        claimsIdentity.Claims.Add(new Claim(item.Key, item.Value.ToString()));
                        if (reader.TokenType == JsonToken.PropertyName)
                        {
                            var ctype = (String) reader.Value;
                            if (reader.Read() && reader.TokenType == JsonToken.String)
                            {
                                claimsIdentity.Claims.Add(new Claim(ctype, reader.Value.ToString()));
                            }
                        }
                    }
                }

                HttpContext.Current.User = claimsPrincipal;
                Thread.CurrentPrincipal = claimsPrincipal;
            }
        }

        public class SimpleClaim
        {
            public String ClaimType { get; set; }

            public String Value { get; set; }
        }
    }
}

As you can see, after successful forms authentication we transform the FormsIndentity into ClaimsIdentity. Additionally we deserialize user data of the forms authentication ticket into claims. I haven’t mentioned yet how I imported claims classes and structures into a .NET4.0 application. I needed to install a Windows Identity Foundation (aka Microsoft.IdentityModel) Nuget package which is a predecessor of the System.IdentityModel assembly. I also added following lines to the web.config file:

  <configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <microsoft.identityModel>
    <service>
      <claimsAuthorizationManager type="MembershipAuth.Authz.AuthorizationManager" />
    </service>
  </microsoft.identityModel>

Our claims authorization manager is quite simple and it only checks if user trying to perfom LoginAsKing action is actually a king:

namespace MembershipAuth.Authz
{
    public class AuthorizationManager : ClaimsAuthorizationManager
    {
        public override bool CheckAccess(AuthorizationContext context)
        {
            var action = context.Action.FirstOrDefault();
            if (action != null && String.Equals(action.Value, "LoginAsKing", StringComparison.Ordinal)) {
                foreach (ClaimsIdentity identity in context.Principal.Identities) {
                    if (identity.Claims.Where(c => String.Equals(c.ClaimType, "urn:usertype", StringComparison.Ordinal)
                            && String.Equals(c.Value, "king", StringComparison.Ordinal)).Any()) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}

Finally it’s time to bind our AuthorizationManager with actions in the controller. For this purpose we will use the Thinktecture.IdentityModel library (available as a Nuget package for .NET4.0 and .NET4.5). It implements a ClaimsAuthorizeAttribute which you can use to apply resource/action based authorization in your application. It’s a much better choice than the framework’s default role based authorization which forces you to mix business and authorization logic (more on this subject can be found in Dominick Baier’s article: http://leastprivilege.com/2014/06/24/resourceaction-based-authorization-for-owin-and-mvc-and-web-api/). Finally it’s time to present our HomeController actions:

namespace MembershipAuth.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index() {
            return Content(User.Identity.IsAuthenticated ? User.Identity.Name : "Anonymous");
        }

        [Authorize]
        public ActionResult Auth() {
            return Content("auth");
        }

        [ClaimsAuthorize("LoginAsKing")]
        public ActionResult ClaimsAuth()
        {
            return Content("authz");
        }
    }
}

Only test user will be allowed to perform ClaimsAuth action as only he claims to be a king :). Both admin and test can call Auth action. As you can see our MembershipAuth application understands cookies generated by the IdentityAuth application and additionally authorizes users based on theirs claims – our goal is achieved.

I strongly encourage you to use Thinktecture.IdentityModel library to implement action/resource based authorization in all your applications. Also if you need to migrate data model from SQL Membership to ASP.NET Identity check out this tutorial. Finally, source code of the MembershipAuth and the IdentityAuth applications is available for download from my blog samples site.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.