Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implementing User Groups using Claims in ASP.NET Identity 2.0

0.00/5 (No votes)
11 Aug 2020 1  
Steps for implementing user groups using Claims
This tip lists the steps required for implementation of user groups using Claims in ASP.NET Identity 2.0. These steps include database modifications, actions storage, control actions in groups, authorization verification ClaimsAuthorizeAttribute, and claims assigning.
  • Download source code from Github

Introduction

ASP.NET Identity is the membership system for authentication and authorization of the users by building an ASP.NET application.

Authentication is used by the server to determine who is accessing their information or website. In authentication, the user or customer must prove their identity on a web server by log-in using email and word or using various social providers.

Authorization is a process by which a server determines if the client has permission to use a resource or access a file after successful authentication.

For more details, read the original article.

ASP.NET Identity is using only Roles and Claims to achieve Authorization, in some applications if your business logic needs an extra layer of authorization management like User Groups you always try to achieve this away from the ASP.NET Identity as natively they don't provide tables or methods to achieve this layer.

Background

ASP.NET Identity is providing natively a default schema where you can add any extension tables as the following schema:

Image 1

Using the Code

This implementation consists of the following steps:

Step 1: Database Modifications

To introduce the User Group feature, of course, you need to add some tables to store this information.

In this implementation, we need to add only the following tables:

  1. tblGroups {PK_Id, Name}
  2. tblGroupActions {PK_Id, FK_Group, ActionName}
  3. tblUserGroups {PK_Id, FK_Group, FK_User}

Image 2

But where is tblActions? That is the trick which will be illustrated in the next section.

Step 2: Actions Storage

Where will my actions be stored? Actually, the Actions are defined in the application layer so it will be redundant to add actions entry in some table every time we create an action.

So if we created a function to retrieve all actions in the application, our job is done. And this can be implemented by the following function:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

namespace UserManagmentSystem.Controllers
{
    public class ImplementedMethods
    {
        public List ActiveMethods;

        public  ImplementedMethods()
        {
            var asm = Assembly.GetExecutingAssembly();
            ActiveMethods  = asm.GetTypes()
                .Where(type => typeof(Controller)
                    .IsAssignableFrom(type))
                .SelectMany(type => type.GetMethods())
                .Where(method => method.IsPublic
                    && !method.IsDefined(typeof(NonActionAttribute))
                    && (
                    method.CustomAttributes.Any
                           (s => s.AttributeType == typeof(HttpPostAttribute)) ||
                    method.CustomAttributes.Any
                           (s => s.AttributeType == typeof(HttpGetAttribute))
                    )
                    && (

                    !method.CustomAttributes.Any(s => s.AttributeType == 
                                                 typeof(AllowAnonymousAttribute))
                    )
                    && method.CustomAttributes.Any
                       (s => s.AttributeType == typeof(ClaimsAuthorizeAttribute))
                    && (
                        method.ReturnType == typeof(ActionResult) ||
                        method.ReturnType == typeof(Task) ||

                        method.ReturnType == typeof(String)                       
                        )
                    )
                .Select(m => m.CustomAttributes.FirstOrDefault
                       (s => s.AttributeType == typeof(HttpPostAttribute) || 
                        s.AttributeType == typeof(HttpGetAttribute)).
                        AttributeType.Name.Replace("Attribute", "") + " : " + 
                        m.DeclaringType.ToString().Split('.')[2].Replace
                        ("Controller", "") + "/" + m.Name).ToList();
        }
    }
}

The main objective of the previous function is to retrieve all Controller Methods which are:

Is decorated by GET or POST and not AllowAnonymousAttribute and have a custom decoration of "ClaimsAuthorizeAttribute" which ensure that Action will be added to the pool and also that action will check the claims of the logged-in user

Step 3: Control Actions in Groups

Now, it is easy to create a controller for Groups to {Add, Edit, Delete, Details, AddAction, RevokeAction}:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using AccountingSystem.Models;
using Audit.Mvc;

namespace UserManagmentSystem.Controllers
{
    [Authorize]
  
    public class GroupsController : Controller
    {
        private AccountingdbEntities db = new AccountingdbEntities();

        // GET: Groups
        public ActionResult Index()
        {
            return View(db.tblGroups.ToList());
        }

        // GET: Groups/Details/5
        public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            tblGroups tblGroups = db.tblGroups.Find(id);
            if (tblGroups == null)
            {
                return HttpNotFound();
            }

            GroupsViewModel groupsViewModel = new GroupsViewModel();
            ImplementedMethods implementedMethods = new ImplementedMethods();
            groupsViewModel.Name = tblGroups.Name;
            groupsViewModel.PK_Id = tblGroups.PK_Id;
            groupsViewModel.Actions = tblGroups.tblGroupActions.Select
                                      (s => new ActionsViewModel
            {
                PK_Id = s.PK_Id,
                Name = s.ActionName
            }).ToList();
            groupsViewModel.Users = tblGroups.tblUserGroups.Select(s => new UsersViewModel
            {
                Id = s.AspNetUsers.Id,
                Name = s.AspNetUsers.UserName,
            OrdersConut =  db.tblOrderStatusHistory.Where
                (k => k.FK_User == s.AspNetUsers.Id && k.tblOrderStatus.IsFirst && 
                 k.tblOrderStatus.IsDefault).Count()
           
            }).ToList();
            ViewBag.AvailableActions = implementedMethods.ActiveMethods.Where
                  (t => !db.tblGroupActions.Any(k => k.FK_Group == id && 
                   k.ActionName == t)).Select(s => new SelectListItem 
                  { Value = s, Text = s }).ToList();

            return View(groupsViewModel);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult RevokeAction(int GroupId, string ActionName)
        {
            tblGroupActions tblGroupActions = 
                   db.tblGroupActions.FirstOrDefault
                   (s=> s.FK_Group == GroupId && s.ActionName == ActionName);
            if (tblGroupActions!= null)
            {
                db.tblGroupActions.Remove(tblGroupActions);
                db.SaveChanges();
            }           

            return RedirectToAction("Details", new { id = GroupId });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult AddAction(int GroupId, string ActionName)
        {
            tblGroupActions tblGroupActions = new tblGroupActions() 
                          { ActionName = ActionName, FK_Group = GroupId };
            if (tblGroupActions != null)
            {
                db.tblGroupActions.Add(tblGroupActions);
                db.SaveChanges();
            }

            return RedirectToAction("Details", new { id = GroupId });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult RevokeUser(int GroupId, string UserId)
        {
            tblUserGroups tblUserGroups = db.tblUserGroups.FirstOrDefault
                               (s => s.FK_Group == GroupId && s.FK_User == UserId);
            if (tblUserGroups != null)
            {
                db.tblUserGroups.Remove(tblUserGroups);
                db.SaveChanges();
            }

            return RedirectToAction("Details", new { id = GroupId });
        }
    
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Step 4: Authorization Verification "ClaimsAuthorizeAttribute"

By applying this custom AuthorizeAttribute, we can ensure that the application will check if CurrentUser claims have the claim of this action.

The wonderful thing about Claims and Roles in Identity is that in the Current login Session, the UserManager will store User claims and Roles in the Memory, so there is no need to hit the database every time for checking for claims:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;

namespace UserManagmentSystem.Controllers
{
    public class ClaimsAuthorizeAttribute : AuthorizeAttribute
    {
        private string claimType;

        public ClaimsAuthorizeAttribute(string type)
        {
            this.claimType = type;
        }
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            var user = filterContext.HttpContext.User as ClaimsPrincipal;
            if (user != null && user.HasClaim(claimType, claimType))
            {
                base.OnAuthorization(filterContext);
            }
            else
            {
                base.HandleUnauthorizedRequest(filterContext);
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace UserManagmentSystem.Controllers
{
    [Authorize]
    
    public class CarriersController : Controller
    {
        [ClaimsAuthorize("HttpGet : Carriers/Index")]
        [HttpGet]
        public ActionResult Index()
        {
           //
        }

        [ClaimsAuthorize("HttpGet : Carriers/Details")]
        [HttpGet]
        public ActionResult Details(int? id)
        {
            //
        }

        [ClaimsAuthorize("HttpGet : Carriers/Create")]
        [HttpGet]
        public ActionResult Create()
        {
           //
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [ClaimsAuthorize("HttpPost : Carriers/Create")]
        public ActionResult Create(Model)
        {
           //
        }       
    }
}

Step 5: Claims Assigning

In this implementation, we found that it is better to assign user claims by sign-in as any change in the tblGroupActions table will affect only after signing in.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    var user = UserManager.Find(model.Email, model.Password);
    if (user != null)
    {
        var UserClaim = user.Claims.AsEnumerable().ToList();

        foreach (IdentityUserClaim claim in UserClaim)
        {
            await UserManager.RemoveClaimAsync
                  (user.Id, new Claim(claim.ClaimType, claim.ClaimValue));

        }
        var UserClaims = db.tblGroupActions.AsEnumerable().Where
              (s => s.tblGroups.tblUserGroups.Any(l => l.FK_User == user.Id))
            .Select(k => new Claim(k.ActionName, k.ActionName)).ToList();

        foreach (Claim cliam in UserClaims)
        {
            await UserManager.AddClaimAsync(user.Id, cliam);
        }
    }
    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout,
    // change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync
      (model.Email, model.Password, model.RememberMe, shouldLockout: true);
    switch (result)
    {
        case SignInStatus.Success:

            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode",
                   new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Adding Actions for a certain Group:

Image 3

History

  • 11th August, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here