Implementing User Management with ASP.NET Core Identity and custom claims

The article shows how to implement user management for an ASP.NET Core application using ASP.NET Core Identity. The application uses custom claims, which need to be added to the user identity after a successful login, and then an ASP.NET Core policy is used to authorize the identity.

Code: https://github.com/damienbod/AspNetCoreAngularSignalRSecurity

Setting up the Project

The demo application is implemented using ASP.NET Core MVC and uses the IdentityServer and IdentityServer4.AspNetIdentity NuGet packages.

ASP.NET Core Identity is then added in the Startup class ConfigureServices method. SQLite is used as a database. A scoped service for the IUserClaimsPrincipalFactory is added so that the additional claims can be added to the Context.User.Identity scoped object.

An IAuthorizationHandler service is added, so that the IsAdminHandler can be used for the IsAdmin policy. This policy can then be used to check if the identity has the custom claims which was added to the identity in the AdditionalUserClaimsPrincipalFactory implementation.

public void ConfigureServices(IServiceCollection services)
{
	...
	
	services.AddDbContext<ApplicationDbContext>(options =>
	 options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

	services.AddIdentity<ApplicationUser, IdentityRole>()
	 .AddEntityFrameworkStores<ApplicationDbContext>()
	 .AddDefaultTokenProviders();

	services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, 
	 AdditionalUserClaimsPrincipalFactory>();

	services.AddSingleton<IAuthorizationHandler, IsAdminHandler>();
	services.AddAuthorization(options =>
	{
		options.AddPolicy("IsAdmin", policyIsAdminRequirement =>
		{
			policyIsAdminRequirement.Requirements.Add(new IsAdminRequirement());
		});
	});

	...
}

The application uses IdentityServer4. The UseIdentityServer extension is used instead of the UseAuthentication method to use the authentication.

public void Configure(IApplicationBuilder app, 
  IHostingEnvironment env, 
  ILoggerFactory loggerFactory)
{
	...
	
	app.UseStaticFiles();

	app.UseIdentityServer();

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

The ApplicationUser class implements the IdentityUser class. Additional database fields can be added here, which will then be used to create the claims for the logged in user.

using Microsoft.AspNetCore.Identity;

namespace StsServer.Models
{
    public class ApplicationUser : IdentityUser
    {
        public bool IsAdmin { get; set; }
        public string DataEventRecordsRole { get; set; }
        public string SecuredFilesRole { get; set; }
    }
}

The AdditionalUserClaimsPrincipalFactory class implements the UserClaimsPrincipalFactory class, and can be used to add the additional claims to the user object in the HTTP context. This was added as a scoped service in the Startup class. The ApplicationUser is then used, so that the custom claims can be added to the identity.

using IdentityModel;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using StsServer.Models;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace StsServer
{
    public class AdditionalUserClaimsPrincipalFactory 
          : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            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);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>
            {
                new Claim(JwtClaimTypes.Role, "dataEventRecords"),
                new Claim(JwtClaimTypes.Role, "dataEventRecords.user")
            };

            if (user.DataEventRecordsRole == "dataEventRecords.admin")
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.admin"));
            }

            if (user.IsAdmin)
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
            }
            else
            {
                claims.Add(new Claim(JwtClaimTypes.Role, "user"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Now the policy IsAdmin can check for this. First a requirement is defined. This is done by implementing the IAuthorizationRequirement interface.

using Microsoft.AspNetCore.Authorization;
 
namespace StsServer
{
    public class IsAdminRequirement : IAuthorizationRequirement{}
}

The IsAdminHandler AuthorizationHandler uses the IsAdminRequirement requirement. If the user has the role claim with value admin, then the handler will succeed.

using Microsoft.AspNetCore.Authorization;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace StsServer
{
    public class IsAdminHandler : AuthorizationHandler<IsAdminRequirement>
    {
        protected override Task HandleRequirementAsync(
          AuthorizationHandlerContext context, IsAdminRequirement requirement)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            if (requirement == null)
                throw new ArgumentNullException(nameof(requirement));

            var adminClaim = context.User.Claims.FirstOrDefault(t => t.Value == "admin" && t.Type == "role"); 
            if (adminClaim != null)
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

The AdminController adds a way to do the CRUD operations for the Identity users. The AdminController uses the Authorize attribute with the policy IsAdmin to authorize. The AuthenticationSchemes needs to be set to “Identity.Application”, because Identity is being used. Now admins can create, or edit Identity users.

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using StsServer.Data;
using StsServer.Models;

namespace StsServer.Controllers
{
    [Authorize(AuthenticationSchemes = "Identity.Application", Policy = "IsAdmin")]
    public class AdminController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly UserManager<ApplicationUser> _userManager;

        public AdminController(ApplicationDbContext context, UserManager<ApplicationUser> userManager)
        {
            _context = context;
            _userManager = userManager;
        }

        public async Task<IActionResult> Index()
        {
            return View(await _context.Users.Select(user => 
                new AdminViewModel {
                    Email = user.Email,
                    IsAdmin = user.IsAdmin,
                    DataEventRecordsRole = user.DataEventRecordsRole,
                    SecuredFilesRole = user.SecuredFilesRole
                }).ToListAsync());
        }

        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var user = await _context.Users
                .FirstOrDefaultAsync(m => m.Email == id);
            if (user == null)
            {
                return NotFound();
            }

            return View(new AdminViewModel
            {
                Email = user.Email,
                IsAdmin = user.IsAdmin,
                DataEventRecordsRole = user.DataEventRecordsRole,
                SecuredFilesRole = user.SecuredFilesRole
            });
        }

        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
         [Bind("Email,IsAdmin,DataEventRecordsRole,SecuredFilesRole")] AdminViewModel adminViewModel)
        {
            if (ModelState.IsValid)
            {
                await _userManager.CreateAsync(new ApplicationUser
                {
                    Email = adminViewModel.Email,
                    IsAdmin = adminViewModel.IsAdmin,
                    DataEventRecordsRole = adminViewModel.DataEventRecordsRole,
                    SecuredFilesRole = adminViewModel.SecuredFilesRole,
                    UserName = adminViewModel.Email
                });
                return RedirectToAction(nameof(Index));
            }
            return View(adminViewModel);
        }

        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var user = await _userManager.FindByEmailAsync(id);
            if (user == null)
            {
                return NotFound();
            }

            return View(new AdminViewModel
            {
                Email = user.Email,
                IsAdmin = user.IsAdmin,
                DataEventRecordsRole = user.DataEventRecordsRole,
                SecuredFilesRole = user.SecuredFilesRole
            });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(string id, [Bind("Email,IsAdmin,DataEventRecordsRole,SecuredFilesRole")] AdminViewModel adminViewModel)
        {
            if (id != adminViewModel.Email)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    var user = await _userManager.FindByEmailAsync(id);
                    user.IsAdmin = adminViewModel.IsAdmin;
                    user.DataEventRecordsRole = adminViewModel.DataEventRecordsRole;
                    user.SecuredFilesRole = adminViewModel.SecuredFilesRole;

                    await _userManager.UpdateAsync(user);
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!AdminViewModelExists(adminViewModel.Email))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(adminViewModel);
        }

        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var user = await _userManager.FindByEmailAsync(id);
            if (user == null)
            {
                return NotFound();
            }

            return View(new AdminViewModel
            {
                Email = user.Email,
                IsAdmin = user.IsAdmin,
                DataEventRecordsRole = user.DataEventRecordsRole,
                SecuredFilesRole = user.SecuredFilesRole
            });
        }

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(string id)
        {
            var user = await _userManager.FindByEmailAsync(id);
            await _userManager.DeleteAsync(user);
            return RedirectToAction(nameof(Index));
        }

        private bool AdminViewModelExists(string id)
        {
            return _context.Users.Any(e => e.Email == id);
        }
    }
}

Running the application

When the application is started, the ADMIN menu can be clicked, and the users can be managed by administrators.

Links

http://benfoster.io/blog/customising-claims-transformation-in-aspnet-core-identity

https://adrientorris.github.io/aspnet-core/identity/extend-user-model.html

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio

Advertisements

3 comments

  1. Fiyaz Bin Hasan · · Reply

    Nicely written. What I was looking for 🙂

  2. […] Implementing User Management with ASP.NET Core Identity and custom claims (Damien Bowden) […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: