DarksideCookie

Come to the dark side...we have cookies!

Claims-based identities in ASP.NET MVC 4.5 using the standard ASP.NET providers

Lately I have done a bit of work with claims-based identities. Most of it has been about doing federated security using the Windows Azure Access Control Service. However, I have also been working with a client that wanted claims-based identity management without federating it. For the moment, they just want to run locally, but they want to be prepared for a future where they might expand and move to a federated paradigm. And also, the way that they handle multitenancy is a perfect fit for claims…

Interestingly enough, working through their scenario, I found that there is a lot of information on the web about how to set up claims-based identity management using federation, but there is not a whole lot around for running it locally… It might not be that surprising considering that federated security has some really good points. Having been faced with this lack of information, I had to come up with a solution on my own, and building on what I built for them, I decided to create an extended example…

Massive disclaimer first! I have limited experience in the newer features of ASP.NET MVC, so some of the things I am building here might already be built in, but I don’t care…building this gave me some good insight, so I’m going to go ahead and blog it anyway…

As of .NET 4.5, Windows Identity Foundation has been moved from an external library to being a part of the framework. And ClaimsPrincipal is now the the base class for the principals being used in ASP.NET. So it is pretty obvious that Microsoft believes that this is the future…

Anyhow…let’s get started! The first thing to do is to create a new empty MVC 4.5 project and get it secured, which is done like this.

After the project has been created, the web.config has to be changed to enable the “new”  security stuff. First of all, 2 new sections need to be added to the top of the file like this

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <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>
  ...
</configuration>

This will enable the configuration of the former WIF stuff…hmm..I don’t know what to call it now…is it still WIF? All the code I will be writing uses the FederatedAuthentication class, but I don’t want to bring in federation, so that seems wrong… I will just keep calling it WIF. If that is wrong, let me know…

Ok, now that those sections are in there, I can configure it if needed. Luckily, in this scenario, I am building really simple things, so the configuration needed is tiny. Normally it is much more complicated as we need to configure federation. And to be honest, the only reason that I need any configuration at all is that I will be running this without SSL for this demo… So I need to add this config

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

The final thing I need to configure to get the WIF stuff done and dusted, is to add an HttpModule to the request pipe. The module in question is called SessionAuthenticationModule, and it is added like this

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

Ok…that’s it…all that is left from a configuration point of view is to secure the site. This is done using old-fashioned forms authentication. So I set up the site to use Forms authentication, and deny access to unauthorized users like this

<system.web>
    <authentication mode="Forms">
        <forms loginUrl="/Auth/Login" defaultUrl="/">
            <credentials passwordFormat="Clear">
                <user name="Admin" password="changeme" />
            </credentials>
        </forms>
    </authentication>
    <authorization>
        <deny users="?" />
    </authorization>
</system.web>

As you can see, I have set it up so that the login page is at “/Auth/Login”, and the default Url is the root of the site. I have also added a hardcoded set of credentials just to have a way to test my solution before I get user management going. This will be replaced by the membership provider later…

Ok, that’s it for configuration… The next thing is to create a home page. So I create a basic HomeController with a single Index() action, and a corresponding view that has this bit of HTML

<!DOCTYPE html>
 
<html>
<head>
    <title>Welcome</title>
</head>
<body>
    <div>
        <h1>Welcome @User.Identity.Name</h1>
        <div>
            <h2>Claims</h2>
            @foreach (var claim in ((ClaimsPrincipal) User).Claims)
            {
                <div>@claim.Type : @claim.Value</div>
            }
        </div>
        <br/>
        @Html.ActionLink("Log Out", "SignOut", "Auth")
    </div>
</body>
</html>

Ok, next up is the AuthController, with its 2 Login() actions and a SignOut() action. (This will be extended with registration later on, but let’s start here…)

The first Login() action returns a view that has a textbox for username and a password box for the password. This view posts the login information to the second Login() action, which is responsible for the real functionality… This action takes a LoginModel which is a basic model offering Username and Password properties.

The controller starts by validating the modelstate, and if it succeeds, it uses the FormsAuthentication class to authenticate the user based on the hardcoded credentials from the web.config file. If the credentials are ok, it creates a set of basic claims, which are used to create a new ClaimsIdentity, which in turn is used to create a ClaimsPrincipal, which in turn is used to create a SessionSecurityToken, which in turn is written to a cookie using the FederatedAuthentication class. After this, the user is redirected back to the home page.

Ok, that was a lot of “which in turn”…the code is quite simple

[HttpPost]
public ActionResult Login(LoginModel model)
{
    if (ModelState.IsValid)
    {
        if (FormsAuthentication.Authenticate(model.Username, model.Password))
        {
            var claims = new[]
                        {
                            new Claim(ClaimTypes.NameIdentifier, model.Username), 
                            new Claim(ClaimTypes.Name, model.Username)
                        };
            var identity = new ClaimsIdentity(claims, "Forms");
            var principal = new ClaimsPrincipal(identity);
 
            var token = new SessionSecurityToken(principal);
 
            FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(token);
 
            return Redirect(FormsAuthentication.DefaultUrl);
        }
        ModelState.AddModelError("IncorrectData", "Could not validate username and/or password");
    }
    return View(model);
}

The reason that I am setting both the ClaimTypes.NameIdentifier and the ClaimTypes.Name is that the ClaimTypes.Name is used by the system to set the Name property of the users identity, but I really want the NameIdentifier as the “key” for my user.

Ok, that’s it! Except for the SignOut() action which is almost a one-liner

public ActionResult SignOut()
{
   FederatedAuthentication.SessionAuthenticationModule.SignOut();
   return RedirectToAction("Login");
}

Running this application will redirect you to the login page where you can login. Logging in will generate a new token cookie and then redirect you back to the home page showing you the users name. Unfortunately, this is hard to show in a blog, but it works…trust me.

Unfortunately, this is a very crappy way of doing things… You can only add users in the web.config, which sucks, which is the reason why one should use the membership provider instead.

You are free to have what ever opinion you want regarding the providers I am using, but they are quick to set up, and work in basic scenarios! I agree that they have some design flaws, but I don’t care! They are there, and ready to use…

To start using the ASP.NET providers, I need a database. So I fire up my SQL Server Management Studio and create a new database named AspNetDb. Once that database is created, I can create the required tables by executing the aspnet_regsql.exe executable using the Visual Studio command prompt. Next I create a user called “webuser” and give it full access to the created tables in AspNetDb database using the predefined database roles.

Ok, now that the database is done, I guess it is time to configure the providers, or at least the membership provider. I will start by walking through the authentication part, and then add roles and custom claims later.

Configuring the membership provider is basic stuff, and well documented, so I won’t go through that. My configuration looks like this

<connectionStrings>
    <add name="AspNetDb" connectionString="Data Source=.;Initial Catalog=AspNetDb; User Id=webuser; Password=webuser;" />
</connectionStrings>
<membership defaultProvider="SqlProvider">
  <providers>
    <clear />
    <add name="SqlProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="AspNetDb" 
         applicationName="DarksideCookie.AspNet.FedAuth.Local" enablePasswordRetrieval="false" enablePasswordReset="true" 
         requiresQuestionAndAnswer="false" requiresUniqueEmail="true" passwordFormat="Hashed" 
         minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="3" />
  </providers>
</membership>

There is a lot of config in there, I know, but it is only because I wanted to decrease the somewhat ridiculous levels of security the provider puts on the password by default…

Time to update the Login() action. I replace the FormsAuthentication stuff with the Membership class instead. Other than that, it looks pretty much the same

if (ModelState.IsValid)
{
    if (Membership.ValidateUser(model.Username, model.Password))
    {
        var user = Membership.GetUser(model.Username);
 
        var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.NameIdentifier, user.Email), 
                        new Claim(ClaimTypes.Name, user.UserName)
                    };
        var identity = new ClaimsIdentity(claims, "Forms");
        var principal = new ClaimsPrincipal(identity);
        var token = SessionSecurityToken(principal);
 
        FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(token);
        return Redirect(FormsAuthentication.DefaultUrl);
    }
    
    ModelState.AddModelError("IncorrectData", "Could not validate username and/or password");
}
return View(model);

Ok…sweet! That was a simple change! Let’s try it out!

Oh…ehh…I need a user to login. I guess I need a way to register as well!

To do this, I add 2 new actions called Register() to the AuthController, and a simple registration page that looks like this

<!DOCTYPE html>
 
<html>
<head>
    <title>Register</title>
</head>
<body>
    <div>
        <div>
            <b>@Html.ValidationSummary()</b>
        </div>
        @using (Html.BeginForm())
        {
            <div>
                @Html.LabelFor(model => model.Username)
                @Html.TextBoxFor(model => model.Username)
            </div>
            <div>
                @Html.LabelFor(model => model.Email)
                @Html.TextBoxFor(model => model.Email)
            </div>
            <div>
                @Html.LabelFor(model => model.Password)
                @Html.PasswordFor(model => model.Password)
            </div>
            <div>
                @Html.LabelFor(model => model.RepeatPassword)
                @Html.PasswordFor(model => model.RepeatPassword)
            </div>
            <div>
                <input type="submit" value="Register" />
            </div>
        }
    </div>
</body>
</html>

The action that takes the posted login data validates the data, and then uses the Membership class to create a new user. However, to be nice, I also sign in the user before redirecting the user to the homepage. Like this

[HttpPost]
public ActionResult Register(RegisterModel model)
{
   if (ModelState.IsValid)
   {
       if (model.Password != model.RepeatPassword)
       {
           ModelState.AddModelError("RepeatPasswordError", "Could not verify password...");
           return View(model);
       }
 
       var user = Membership.CreateUser(model.Username, model.Password, model.Email);
 
       var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.NameIdentifier, user.Email), 
                        new Claim(ClaimTypes.Name, user.UserName)
                    };    
       var identity = new ClaimsIdentity(claims, "Forms");
       var principal = new ClaimsPrincipal(identity);
       var token = new SessionSecurityToken(principal);
 
       FederatedAuthentication.SessionAuthenticationModule
            .WriteSessionTokenToCookie(GetSecurityTokenForMembershipUser(token));
       return Redirect(FormsAuthentication.DefaultUrl);
   }
   return View(model);
}

Ok, that wasn’t too hard! And yes, there is some code-duplication in here, but that is just for the simplicity of the post. In the sample code, this has been refactored out, don’t worry! :)

The last step in enabling the registration is to let people access it without being authenticated. This is easily done by adding a <location /> element to the web.config, and allow anonymous access. Like this

<location path="Auth/Register">
    <system.web>
        <authorization>
            <allow users="*" />
        </authorization>
    </system.web>
</location>

Now there is a way to register, as well as a way to login. After testing that it all works, which I still can’t show on the blog, it is time to add some role functionality…

To configure the role provider, I add the following config to my web.config

<roleManager cacheRolesInCookie="false" defaultProvider="SqlProvider" enabled="true">
  <providers>
    <clear/>
    <add connectionStringName="AspNetDb" applicationName="DarksideCookie.AspNet.FedAuth.Local" name="SqlProvider"
         type="System.Web.Security.SqlRoleProvider" />
  </providers>
</roleManager>

The important things to note here is that I set “enabled” to true, but turn off cookie caching. Why? Well, because the role information will be set in the token anyway, so there is no need to add 2 cookies for that…

In this very simple demo, I leave it up to the user to define what roles he/she wants to be member of as a part of the registration process. This is potentially not the most common scenario, but it makes it simple. Once that this has been added, I can modify the Register() action to add the user to the defined roles as well. However, before adding the user to the role, i make sure that the role is available, and if not, I add it.

The modification means adding the following code right after the user has been created

...
foreach (var role in model.Roles.Where(x => x.Checked))
{
    if (!Roles.RoleExists(role.Name))
        Roles.CreateRole(role.Name);
 
    Roles.AddUserToRole(model.Username, role.Name);
}
...

Here is a funky little kicker though. As long as the user has roles defined, the ClaimsPrincipal and the SessionSecurityToken will magically work together and automatically generate the role claims for me.

Ok, trying this out will prove that the role claims are added as before, but also the role claims. Cool…a little confusing…but cool. I actually added them manually when I first built it, but got duplicates. So I removed my code and it just worked…

So, now I have authentication using the membership provider and roles using the role provider without doing any plumbing code or database work…kind of neat. The only thing left now is to add custom claims using the profile provider…

Configuring the profile provider is pretty much identical to the previous provider configurations, with one exception, I also have to define what profile properties I want to have available.

<profile defaultProvider="SqlProvider">
  <providers>
    <clear/>
    <add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider"
         connectionStringName="AspNetDb" applicationName="DarksideCookie.AspNet.FedAuth.Local" />
  </providers>
  <properties>
    <add name="Organization" type="String" customProviderData="http://chris.59north.com/claims/organization/" />
  </properties>
</profile>

In this case, I define a single profile property called “Organization” of type string. The profile configuration also offers the ability to add custom data to each of the properties using an attribute called customProviderData. Normally, this attribute is supposed to be used by custom profile providers to do custom work or whatever, but in this case, I’m hijacking that attribute to add the name of the claim that it corresponds to.

Next I modify the registration page and model to include an Organization property, and then I head to my AuthController to update the registration and token creation code.

During the registration, I create a new profile using the ProfileBase class, and set the profile property before saving the new profile to the database. This is done by adding the following code right after the code for adding the roles

var profile = ProfileBase.Create(user.UserName, true);
profile.SetPropertyValue("Organization", model.Organization);
profile.Save();

While creating the claims for the session token, I bring up the profile and use the values in it to populate the claims. Notice the use of the property.Attributes[“CustomProviderData”] to get hold of the claim type name.

var profile = ProfileBase.Create(user.UserName, true);
foreach (SettingsProperty property in ProfileBase.Properties)
{
    claims.Add(new Claim(property.Attributes["CustomProviderData"].ToString(), profile[property.Name].ToString()));
}

Ok, that’s it! Registering a new user and logging in, I am now faced with a page that looks like this

image

Yes, I do need to create a new account in this case, as the profile is populated at registration. If you really don’t want to create a new user because you love the username you used before, you can just open the database and clean out the existing user and roles…

That’s all folks! Claims-based identity management using the ASP.NET providers instead of federation. This might seem unnecessary, but it does set you up nicely for the future. Having claims-based authentication and authorization offers a quick route to enable federated security if needed.

A little side-note though… Don’t add too many claims. They are sent back and forth as a cookie with each call, so adding a lot of them will slow down the communication…

And as usual, there is obviously code for this. A complete solution is available for download here:DarksideCookie.AspNet.FedAuth.Local.zip (3.74 mb)

Sorry for the hefty download! ASP.NET MVC adds quite a few NuGet packages, bloating the solution a LOT, and I am not sure if it is safe to ditch the NuGet packages and get them re-downloaded when opening the solution… Sorry… I suck at NuGet…

Just remember that you have to configure the database as described, and update the database connection in the web.config.

Cheers!

Posted: Mar 27 2013, 15:52 by ZeroKoll | Comments (1) |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: .NET development | Security
Manage post: :)

Comments (1) -

yogee United States said:

nice article Smilethx...

# May 27 2013, 13:01

Add comment




  Country flag
biuquote
Loading