CheapASPNETHostingReview.com | Best and cheap ASP.NET Core 1.0 hosting. 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 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
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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:
1 2 3 4 5 | // 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
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:
- Add-Migration “Age” <press Enter> to create an upgrade script for our modification.
- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 | ... <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> ... |
5. Update the Controllers\AccountController.cs Register Method
Update the Controllers\AccountController.cs Register method to pass Birthday:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // 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>.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 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.
Claim BirthDay requires more checks, so I will implement verification of the Claim just for demonstration purposes in the Controllers\HomeController.cs About method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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.
Reviewing all the Claims (in debug window):
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.