Search code examples
c#asp.net-identity-2asp.net-mvc-5.2

How do I access role objects from HttpContext, or more specifically from a custom authorize attribute?


Background

I would like to authorize an ApplicationUser based on their ApplicationRoles' RoleClaims. I would like to implement this like so:

public class RoleClaimAuthorizeAttribute : AuthorizeAttribute
{
    public RoleClaim RoleClaim { get; set; }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        foreach (ApplicationRole role in Roles)
        {
            if ((RoleClaim & role.Claims) > 0)
            {
                return true;
            }
        }
        return false;
    }
}

Then I could decorate controller actions like so:

    [RoleClaimAuthorize(RoleClaim = 
        RoleClaim.CanCreateRoles | 
        RoleClaim.CanReadRoles | 
        RoleClaim.CanDeleteRoles | 
        RoleClaim.CanUpdateRoles
    )]
    //
    // GET: /Roles/
    public ActionResult Index()
    {
        return View(_roleManager.Roles);
    }

Question

The problem I'm encountering is any method I can find of reaching an ApplicationUser's ApplicationRoles from my custom authorize attribute returns a string array of ApplicationRole.Name not an array of ApplicationRole so I can't get to ApplicationRole.Claims. I'm also using Unity instead of Owin to handle ApplicationRoleManager so I can't request ApplicationRoleManager via HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>().

So how can I get to a collection of ApplicationRole objects for the current user and thus ApplicationRole.Claims?

Or if it's a more appropriate solution, how can I store a string array of the current ApplicationUser's ApplicationRoles' RoleClaims in HttpContext much like how roles are stored? I'm aware my authorization attribute couldn't work as described in this situation but it's a situation I can work with nonetheless.


Relevant Classes

ApplicationUser

// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser<Guid, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    public override Guid Id { get; set; }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, Guid> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, Guid> manager, string authenticationType)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
        // Add custom user claims here
        return userIdentity;
    }
}

ApplicationRole

public class ApplicationRole : IdentityRole<Guid, ApplicationUserRole>
{
    public ApplicationRole() : base()
    {
        this.Id = Guid.NewGuid();
    }

    public ApplicationRole(string name)
        : this()
    {
        this.Name = name;
    }

    public ApplicationRole(string name, params string[] claims)
        : this(name)
    {
        Claims = (RoleClaim)Enum.Parse(typeof(RoleClaim), String.Join(",", claims));
    }

    public RoleClaim Claims { get; set; }
}

RoleClaim

[Flags]
public enum RoleClaim : int
{
    CanCreateUsers = 1,
    CanReadUsers = 2,
    CanUpdateUsers = 4,
    CanDeleteUsers = 8,
    CanCreateRoles = 16,
    CanReadRoles = 32,
    CanUpdateRoles = 64,
    CanDeleteRoles = 128,
    CanCreateTests = 256,
    CanReadTests = 512,
    CanUpdateTests = 1024,
    CanDeleteTests = 2048
}

ApplicationRoleManager

public class ApplicationRoleManager : RoleManager<ApplicationRole, Guid>
{
    public ApplicationRoleManager(IRoleStore<ApplicationRole, Guid> store) : base(store)
    {
    }
}

Solution

  • If you registered the role manager in Unity you could retrieve everywhere, including your custom attribute, simply by calling following method:

    var roleManager = DependencyResolver.Current.GetService<ApplicationRoleManager>();
    

    Or if you don't want to use the resolver directly you could use Unity's property injection feature so Unity automatically injects the role manager in the custom attribute which in explained here. Then call roleManager.FindByNameAsync() method to retrieve the role object.

    But this approach is not recommended because in each call your code hits the database to retrieve claims. It is much better to store user's claims in ClaimsIdentity when user signs in then retrieve them within the attribute like this:

    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        private readonly ApplicationRoleManager _roleManager;
    
        public ApplicationSignInManager(ApplicationUserManager userManager, 
            IAuthenticationManager authenticationManager,
            ApplicationRoleManager rolemanager)
                : base(userManager, authenticationManager)
        {
             //inject the role manager to the sign in manager
            _roleManager=rolemanager;
        }
    
        public override async Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            var ident= await user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
            // add your custom claims here
            var userRoles=user.Roles.Select(r=>r.RoleId);
            ident.AddClaims(_roleManager.Roles.Where(r => userRoles.Any(ur => ur == r.Id))
                .Select(r=>r.Claims).ToList()
                .Select(c => new Claim("RoleClaims", c.ToString())));
            return ident;
        }
    }
    

    Now RoleClaims are added as claims when user signs in. You could retrieve them in the attribute like this:

    public class RoleClaimAuthorizeAttribute : AuthorizeAttribute
    {
        public RoleClaim RoleClaim { get; set; }
    
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            foreach (var claims in GetClaims(httpContext.User.Identity as ClaimsIdentity))
            {
                if ((RoleClaim & claims) > 0)
                {
                    return true;
                } 
            }
            return false;
        }
        private IEnumerable<RoleClaim> GetClaims(ClaimsIdentity ident)
        {
            return ident==null
                ? Enumerable.Empty<RoleClaim>()
                : ident.Claims.Where(c=>c.Type=="RoleClaims")
                    .Select(c=>(RoleClaim)Enum.Parse(typeof(RoleClaim), c.Value)); 
        }
    }