11 minute read

About an year ago, I had a chance to work on ASP.NET MVC and Claims-based identities for an enterprise application.   Claims-based identity, though introduced a decade ago, has got more focus in .NET world only after Microsoft introduced Windows Identity Foundation (WIF).  With WIF/Claims, achieving loose coupling between authentication models (forms, windows, etc.) and claim management has become fairly simple and robust.  This article provides the  easiest way to implement Claims Identity for an Internet application that uses SimpleMembershipProvider.  However, this example can be used for Intranet Applications for enterprises as well.

This article assumes that you are aware of the concept of claims.  If you wish to revisit the fundamentals of claims, you can refer to An Introduction to Claims on MSDN.

For this article, I would be using ASP.NET MVC 4 Razor-engine with .NET 4.5 framework; however, you can choose to implement this solution on any older version of ASP.NET MVC or .NET.   With .NET 4.5, Windows Identity Foundation from being a separate framework has moved to be a part of .NET framework.   So if your web application is built on an older version of .NET, you will have to install WIF separately and reference its assemblies.

So once you have created your Intranet-ASP.NET MVC 4 application with Razor engine, Visual Studio 2012 will automatically create default folders (Controllers, Views, Content, etc.).

Application Configuration

The first step requires altering web.config to reference WIF

<configSections>
  <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>
<configSections>


For this example, let’s keep things simple and assume that your application does not interact with Third-Party Applications and hence does not require any federation.  If your application requires claims-identity using federation then the configuration below would be little more complex.  The important setting here is “requireSsl” which is set to false.

<system.identityModel.services>
  <federationConfiguration>
      <cookieHandler requireSsl="false" persistentSessionLifetime="2"/>
  </federationConfiguration>
</system.identityModel.services>


The next in configuration is to add a HTTP module that uses Claims-identity for session management.  This SessionAuthenticationModule will be later used to manage sessions and to create/update Claims-Identity cookies

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <add name="SessionAuthenticationModule"
          type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </modules>
  ....
  </system.webServer>

Any change required on authentication settings?  For this example, we will use Forms Authentication with SimpleMembership to keep the article short and aligned to Claims-identity only.   So there is no change required in the configuration file for authentication.

Note: You can use Windows Authentication, SSO, or any other authentication mode that your application requirements demand.  Configuration may change based on the type of authentication you are using.

 

Authentication Controller

 

Generally authentication involves actions such as Login and LogOff.  If you are using FormsAuthentication, you would also require Register action to allow users to register themselves.  I’ll keep the view part to the minimum and will use the same view that is created when a new ASP.NET MVC 4 application is created using Razor engine.  To create Claims-Identity, I prefer to encapsulate the data in a class called UserNonSensitiveData.  You can extend this class to add user roles, permissions, information, etc.

    [Serializable]
    public class UserNonSensitiveData
    {
        public int UserId { get; set; }
        public string Username { get; set; }
        public string Email { get; set; }

        public UserNonSensitiveData(int userId, string userName, string email)
        {
            this.UserId = userId;
            this.Username = userName;
            this.Email = email;
        }

        public UserNonSensitiveData()  { }
    }


For the register action,  we will first create an account using SimpleMembership (WebSecurity class) and re-login the user.  Once the user is authenticated, we can create retrieve user information in an object of UserNonSensitiveData and create a SessionAuthenticationCookie using the SessionAuthenticationModule defined in web.config

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    WebSecurity.CreateUserAndAccount(model.UserName, model.Password, new { });
                    if (WebSecurity.Login(model.UserName, model.Password))
                    {
                        int userId = WebSecurity.GetUserId(model.UserName);
                        var nonSensitiveCookieData = new UserNonSensitiveData(userId, model.UserName, model.UserName);
                        FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(GetSecurityToken(nonSensitiveCookieData));
                    }
                    return RedirectToAction("Index", "Home");
                }
                catch (MembershipCreateUserException exception)
                {
                    AddError("", ErrorCodeToString(exception.StatusCode));
                }
                catch (Exception e)
                {
                    AddError("", e.Message);
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

This code refers to a method GetSecurityToken which converts a UserNonSensitiveData object  to SessionSecurityToken.  This token will be used to update the session.  Here, NameIdentified plays a very important role as it acts as a “key” to identify the user.  If you have any default roles to be assigned to a user who has registered on the website, you can add them just like we have added email address. 

  1. protected static SessionSecurityToken GetSecurityToken(UserNonSensitiveData nonSensitiveData)
  2. {
  3.     var claims = new List<Claim>
  4.                 {
  5.                     new Claim(ClaimTypes.NameIdentifier, nonSensitiveData.UserId.ToString(CultureInfo.InvariantCulture)),
  6.                     new Claim(ClaimTypes.Name, nonSensitiveData.Username),
  7.                     new Claim(ClaimTypes.Email, nonSensitiveData.Email)
  8.                 };
  9.  
  10.     var identity = new ClaimsIdentity(claims, "Forms");
  11.     var principal = new ClaimsPrincipal(identity);
  12.  
  13.     return new SessionSecurityToken(principal, TimeSpan.FromDays(2));
  14. }

Now, the next important user action is the Login action.  Login action essentially is a subset of Register action.

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Login(LoginModel model, string returnUrl)
        {
            if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password))
            {
                var userId = WebSecurity.GetUserId(model.UserName);
                var nonSensitiveCookieData = new UserNonSensitiveData(userId, model.UserName, model.UserName);
                FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(GetSecurityToken(nonSensitiveCookieData));

                return RedirectToLocal(returnUrl);
            }

            // If we got this far, something failed, redisplay form
            AddError("", "The user name or password provided is incorrect.");
            return View(model);
        }

 

The LogOff action is just 2 liner code to ensure that the cookies are cleared

        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual ActionResult LogOff()
        {
            // For Claims-Cookie
            FederatedAuthentication.SessionAuthenticationModule.SignOut();
            WebSecurity.Logout(); // for SimpleMembership
            return RedirectToAction("Index", "Home");
        }


AntiForgeryToken Compatibility

Since we have changed the token to claims-token, our application must be aware of how the claims have to be identified.  This requires adding one line to Application_Start method in Global.asax.cs

// To ensure that claims authentication works with AntiForgeryToken
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

 

Execution

 

Running this code will take you to the Login page (/Account/Login).  When the user successfully registers/logins, a new token cookie is created and user is redirected to the home page.  This cookie has a lifetime which can be set in the code and in web.config file.

Once this cookie is set, you can use ClaimsPrincipal to get the value of any assigned claim as shown below.  If the cookie has expired, you will not get any claims.

        protected Claim GetClaim(string type)
        {
            return ClaimsPrincipal.Current.Claims.FirstOrDefault(c => c.Type.ToString(CultureInfo.InvariantCulture) == type);
        }

This is one of the ways to using Claims-Identity and SimpleMembershipProvider with ASP.NET MVC.

Extending this application

 

You can replace the SimpleMembershipProvider with any other provider (like SqlProvider, etc.) that suites your application requirement.  If your application uses SSO, you can wire up claims after you have received SSO token.  You can serialize SSO token and store it in claims token as well if you need to re-use the token.

All these changes require minimal code and are totally isolated from the Claims-identity management code.