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
{
public void ConfigureAuth(IAppBuilder app)
{
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:
private void SetFormsAuthCookie(ClaimsIdentity identity) {
var userData = JsonConvert.SerializeObject(identity.Claims.Select
(c => new SimpleClaim { ClaimType = c.Type, Value = c.Value }));
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);
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:
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="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:
<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
:
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;
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:
<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:
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
{
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