Click here to Skip to main content
15,887,776 members
Articles / Security

Common Authentication/Authorization Between .NET4.0 and .NET4.5 Web Applications

Rate me:
Please Sign up or sign in to vote.
4.60/5 (3 votes)
7 Aug 2014MIT4 min read 15.4K   9  
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:

  1. IdentityAuth – the MVC5 application and
  2. 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:

C#
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:

C#
[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:

C#
private void SetFormsAuthCookie(ClaimsIdentity identity) {
    // we need to serialize claims to string
    var userData = JsonConvert.SerializeObject(identity.Claims.Select
    (c => new SimpleClaim { ClaimType = c.Type, Value = c.Value }));
    // then create an auth ticket
    var cookie = FormsAuthentication.GetAuthCookie(identity.Name, false);
    var authTicket = FormsAuthentication.Decrypt(cookie.Value);
    authTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name,
                                               authTicket.IssueDate, authTicket.Expiration,
                                               authTicket.IsPersistent,
                                               userData,
                                               authTicket.CookiePath);
    cookie.Value = FormsAuthentication.Encrypt(authTicket);
    // and place it in authorization cookie
    Response.SetCookie(cookie);
}

...

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

    public String Value { get; set; }
}

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

C#
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:

XML
<system.web>
  <machineKey compatibilityMode="Framework20SP2"
  validationKey="a4c44e321ad34e783fbcc8dd58d469577097e0cef52beba39a36dc11996e06d2d8603f2155975
  bc22fd9367c4d66f7ff80101ad5a3339fad002d0aaadf5f6bdb"
  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:

XML
<system.web>
  <machineKey compatibilityMode="Framework20SP2"
  validationKey="a4c44e321ad34e783fbcc8dd58d469577097e0cef52beba39a36dc11996e06d2d8603f2155975bc
  22fd9367c4d66f7ff80101ad5a3339fad002d0aaadf5f6bdb"
  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:

C#
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;

                if (!String.IsNullOrEmpty(formsIdentity.Ticket.UserData))
                {
                    foreach (var sc in JsonConvert.DeserializeObject<IEnumerable<SimpleClaim>>
                            (formsIdentity.Ticket.UserData))
                    {
                        var c = new Claim(sc.ClaimType, sc.Value);
                        if (!claimsIdentity.Claims.Contains(c))
                        {
                            claimsIdentity.Claims.Add(c);
                        }
                    }
                }

                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 the following lines to the web.config file:

XML
<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 is trying to perform LoginAsKing action is actually a king:

C#
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:

C#
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, the source code of the MembershipAuth and the IdentityAuth applications is available for download from my blog samples site.

Filed under: ASP.NET Security, CodeProject 

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
Poland Poland
Interested in tracing, debugging and performance tuning of the .NET applications.

My twitter: @lowleveldesign
My website: http://www.lowleveldesign.org

Comments and Discussions

 
-- There are no messages in this forum --