ASP.NET Core and Claim-based Security

Monday Dec 19th 2016
Share:

Plunge into the main concepts of building claim-based security on top of a brand-new platform: ASP.NET Core (with .NET Core).

By Taras Kholopkin.

Even though the ASP.NET Web platform and ASP.NET project scaffold have undergone some significant changes, ASP.NET MVC Views and Controllers have faced rather minor transformation in comparison with the shift in skeleton of the Application with the new platform. So here's a brief outline of the latest news in ASP.NET development.

In my previous article, "ASP.NET MVC and Claim-Based Security," I shed some light on the term Claim and Claim-based permissions. To refresh, a Claim is an attribute of identity that may define permissions, whereas permission is just a right to perform an action. Talking about Identity functionality required for building Claim-base security, this time it has been extended and now it resides in the Microsoft.AspNetCore.Identity library.

In this article, I'm going to describe the main concepts of building claim-based security on top of a brand-new platform: ASP.NET Core (with .NET Core). At the same time, I will create an application with similar functionality (as was done in my previous article), highlighting the differences.

Let's get down to work. Create a "Hello World" ASP.NET Core Web application using the .NET Core framework. If you need any help, check out these simple steps.

Claims1
Figure 1: Creating a new ASP.NET Core Web application

Just like in the previous version of ASP.NET MVC, the main job is done. The default Visual Studio .NET Web Project template has already added all the namespaces and assemblies required for our test project. The only thing left is to implement simple functionality to add a new Claim during the user registration/creational process and then apply the authorization restriction to the user with the Claim specified.

Let's quickly review the most important pieces of functionality responsible for security work this time:

Startup.cs is a class for the entire application bootstrap, including security:

public class Startup
{
   // Setting up the hosting environment
   // and configuration
   public Startup(IHostingEnvironment env)
   {
      var builder = new ConfigurationBuilder()
         .SetBasePath(env.ContentRootPath)
         .AddJsonFile("appsettings.json", optional: true,
            reloadOnChange: true)
         .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
            optional: true);

      if (env.IsDevelopment())
      {
         // For more details on using the user secret store see
         // http://go.microsoft.com/fwlink/?LinkID=532709
         builder.AddUserSecrets();
      }

      builder.AddEnvironmentVariables();
      Configuration = builder.Build();
   }

   public IConfigurationRoot Configuration { get; }

   // This method gets called by the runtime. Use this method
   // to add services to the container.
   public void ConfigureServices(IServiceCollection services)
   {
      // Add framework services.
      services.AddDbContext<ApplicationDbContext>(options =>
         options.UseSqlServer(Configuration.GetConnectionString
            ("DefaultConnection")));

      // Add Identity stuff to the application services
      services.AddIdentity<ApplicationUser, IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>()
         .AddDefaultTokenProviders();

      services.AddMvc();

      // Add application services.
      services.AddTransient<IEmailSender, AuthMessageSender>();
      services.AddTransient<ISmsSender, AuthMessageSender>();
   }

   // This method gets called by the runtime. Use this method to
   // configure the HTTP request pipeline.
   public void Configure(IApplicationBuilder app,
      IHostingEnvironment env, ILoggerFactory loggerFactory)
   {
      loggerFactory.AddConsole(Configuration.GetSection
         ("Logging"));
      loggerFactory.AddDebug();

      if (env.IsDevelopment())
      {
         app.UseDeveloperExceptionPage();
         app.UseDatabaseErrorPage();
         app.UseBrowserLink();
      }
      else
      {
         app.UseExceptionHandler("/Home/Error");
      }

      app.UseStaticFiles();

      // Enable the application to use a cookie to store information
      // for the signed-in user and to use a cookie to temporarily
      // store information about a user logging in with a
      // third-party login provider
      app.UseIdentity();

      // Add external authentication middleware below. To configure
      // them, please see http://go.microsoft.com/fwlink/?LinkID=532715

      app.UseMvc(routes =>
      {
         routes.MapRoute(
            name: "default",
            template: "{controller=Home}/
               {action=Index}/{id?}");
      });
   }
}

Models\ApplicationUser.cs contains an ApplicationUser class that derives from Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUser:

   // Add profile data for application users by
   // adding properties to the ApplicationUser class
   public class ApplicationUser : IdentityUser
   {
   }

Until now, it's been empty, so this is where we should add our claims. Let's start applying code changes to demonstrate Claim-based security in real life:

1. Enable Entity Framework Migrations

Enable Entity Framework Migrations if there are any iterative changes to Claims planned. Because ASP.NET Identity uses Code First, auto-migration would be useful to perform database schema updates. More about Code First Migrations may be found here. With the recent update, it should be enabled by default (see the Data\Migrations folder in the project) and the "Enable-Migrations" command is no longer required.

2. Add Relevant Properties

Add all relevant properties to the ApplicationUser class (in file Models\ApplicationUser.cs) to store the Claims. Let's take "BirthDate" and add this property to ApplicationUser. Don't forget to add the using System clause before class definition.

3. Add EF Migration

Add EF migration to the update database with the new field. In the Package Manager Console, perform the following steps:

  1. Add-Migration "Age" <press Enter> to create an upgrade script for our modification.
  2. Update-Database <press Enter> to run a database schema update.

Now, we need to implement the filling out of the Birthday value. To make it more obvious, add a Birthday parameter to the User Registration form in the Models\AccountViewModels\RegisterViewModel.cs RegisterViewModel class:

public class RegisterViewModel
{
   [Required]
   [EmailAddress]
   [Display(Name = "Email")]
   public string Email { get; set; }

   [Required]
   [StringLength(100, ErrorMessage = "The {0} must be
      at least {2} and at max {1} characters long.",
      MinimumLength = 6)]
   [DataType(DataType.Password)]
   [Display(Name = "Password")]
   public string Password { get; set; }

   [DataType(DataType.Password)]
   [Display(Name = "Confirm password")]
   [Compare("Password", ErrorMessage = "The password
      and confirmation password do not match.")]
   public string ConfirmPassword { get; set; }

   [Required]
   [Display(Name = "Date of Birth")]
   [DataType(DataType.Date)]
   public DateTime BirthDate { get; set; }

}

4. Update the Views\Account\Register.cshtml File

Update the Views\Account\Register.cshtml file with the new field:

   ...
   <div class="form-group">
      <label asp-for="BirthDate"
         class="col-md-2 control-label"></label>
      <div class="col-md-10">
         <input asp-for="BirthDate"
            class="form-control" />
         <span asp-validation-for="BirthDate"
            class="text-danger"></span>
      </div>
   </div>
   ...

Claims2
Figure 2: Creating a new account

5. Update the Controllers\AccountController.cs Register Method

Update the Controllers\AccountController.cs Register method to pass Birthday:

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model,
   string returnUrl = null)
{
   ViewData["ReturnUrl"] = returnUrl;
   if (ModelState.IsValid)
   {
      var user = new ApplicationUser { UserName = model.Email,
         Email = model.Email, BirthDate = model.BirthDate };
      var result = await _userManager.CreateAsync(user,
         model.Password);
      if (result.Succeeded)
      {
         // For more information on how to enable account confirmation
         // and password reset, please visit
         // http://go.microsoft.com/fwlink/?LinkID=532713
         // Send an email with this link
         // var code = await
         // _userManager.GenerateEmailConfirmationTokenAsync(user);
         // var callbackUrl = Url.Action("ConfirmEmail",
         // "Account", new { userId = user.Id, code = code },
         // protocol: HttpContext.Request.Scheme);
         // await _emailSender.SendEmailAsync(model.Email,
         //    "Confirm your account",
         //    $"Please confirm your account by clicking this link:
         //    <a href='{callbackUrl}'>link</a>");
         await _signInManager.SignInAsync(user, isPersistent: false);
         _logger.LogInformation(3, "User created a new account
            with password.");
         return RedirectToLocal(returnUrl);
      }
      AddErrors(result);
   }

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

6. Add the Claims

Now, we need to add the Claims. To be more precise, we need a mechanism to add the Claims in ASP.NET Core because Microsoft.AspNetCore.Identity.SignInManager<TUser>, by default, includes only username and user identifier claims. SignInManager<TUser> uses IUserClaimsPrincipalFactory<TUser> to generate ClaimsPrincipal from TUser (in our case, from ApplicationUser).

We need to create our own implementation of IUserClaimsPrincipalFactory<TUser> to add custom claims. To not generate the boilerplate code, simply derive it from the default UserClaimsPrincipalFactory<TUser> which is already implementing IUserClaimsPrincipalFactory<TUser>.

public class CustomClaimsPrincipalFactory :
   UserClaimsPrincipalFactory<ApplicationUser,
   IdentityRole>
{
   public CustomClaimsPrincipalFactory(
      UserManager<ApplicationUser> userManager,
      RoleManager<IdentityRole> roleManager,
      IOptions<IdentityOptions> optionsAccessor) :
         base(userManager, roleManager, optionsAccessor)
   {
   }

   public async override Task<ClaimsPrincipal>
      CreateAsync(ApplicationUser user)
   {
      var principal = await base.CreateAsync(user);

      // Add your claims here
      ((ClaimsIdentity)principal.Identity).
         AddClaims(new[] {
         new Claim(ClaimTypes.DateOfBirth,
            user.BirthDate.ToString())
      });

      return principal;
   }
}

7. Register CustomClaimsPrincipalFactory

We need to register our CustomClaimsPrincipalFactory in Startup.cs after the Identity setup has been added:

// This method gets called by the runtime. Use this method
// to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
   // Add framework services.
   services.AddDbContext<ApplicationDbContext>(options =>
      options.UseSqlServer(Configuration.
         GetConnectionString("DefaultConnection")));

   // Add Identity stuff to the application services.
   services.AddIdentity<ApplicationUser, IdentityRole>()
      .AddEntityFrameworkStores<ApplicationDbContext>()
      .AddDefaultTokenProviders();

   // Add Custom Claims processor
   services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>,
      CustomClaimsPrincipalFactory>();

   services.AddMvc();

   // Add application services.
   services.AddTransient<IEmailSender, AuthMessageSender>();
   services.AddTransient<ISmsSender, AuthMessageSender>();
}

8. Verify the Claim

We have implemented the Claims setup. The only thing left is to verify the Claim. It is a common practice to write custom Authorize filters to verify the availability and particular value of the Claim pair, and then put that filter on the controllers' actions. As an example, you may refer to ClaimsAuthorizeAttribute that I implemented in my previous article, "ASP.NET MVC and Claim-Based Security."

Claim BirthDay requires more checks, so I will implement verification of the Claim just for demonstration purposes in the Controllers\HomeController.cs About method:

public IActionResult About()
{
   ViewData["Message"] = "Your application description page.";

   var user = HttpContext.User;
   if (!user.HasClaim(c => c.Type ==
      System.Security.Claims.ClaimTypes.DateOfBirth))
   {
      ViewBag.Message = "Cannot detect the Age -
         Claim is absent.";
      return View();
   }

   int minAge = 16;
   var dateOfBirth = Convert.ToDateTime(user.FindFirst(c =>
      c.Type == System.Security.Claims.ClaimTypes.DateOfBirth).Value);

   if (calculateAge(dateOfBirth) >= minAge)
   {
      ViewBag.Message = "Your can view this page.";
   }
   else
   {
      ViewBag.Message = "Your cannot view this page -
         your age is bellow permitted one.";
   }


   return View();
}

private int calculateAge(DateTime dateOfBirth)
{
   int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
   if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
   {
      calculatedAge--;
   }
   return calculatedAge;
}

Any Claim may be extracted easily from the HttpContext.User at any point of the project.

Traditionally, let's try to verify if the code works. I registered one account with DOB today (07/14/2016). Here is the result:

Claims3
Figure 3: After entering a too-young age

Reviewing all the Claims (in debug window):

Claims4
Figure 4: Viewing the Claims in the debug window

Conclusion

That was a step-by-step guideline to set up Claim-based security in ASP.NET Core with the help of ASP.NET Core Identity.

Compared to the previous-generation ASP.NET MVC, at first glance implementation of the Claim-based security looks more complicated in ASP.NET Core. Previously, it was possible to add the Claims directly in the ApplicationUser implementation via overriding the GenerateUserIdentityAsync() method. In ASP.NET Core, we need to implement IUserClaimsPrincipalFactory<TUser> that is internally used by SignInManager<TUser>. On the other hand, we've got more structured classes and interfaces implementation in ASP.NET Core, as logically SignInManager should indeed control sign-in processes (including claims) and ApplicationUser should be just an IdentityUser.

One more useful thing that was introduced in ASP.NET Core is Claim-Based Authorization on top of Policies. It simplifies the verification of Claims on Controllers and Methods, thereby providing an ability to group the Claims.

the new ASP.NET Core brings a lot of benefits with its release: a full list of all features is available. If you are very new to ASP.NET Core, it is also recommended to have a look at this short overview on the official ASP.NET Web site.

About the Author

Taras Kholopkin is a senior solutions architect at SoftServe. He is also a frequent contributor to the SoftServe blog. With more than 10 years in the industry, he has extensive experience within the U.S. IT market. He is responsible for software architectural design and development of enterprise and SaaS solutions (including healthcare solutions).

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved