ASP.NET MVC - Alternative to Role Provider?

16,464

Solution 1

I'm in the same boat as you - I've always hated the RoleProviders. Yeah, they're great if you want to get things up and running for a small website, but they're not very realistic. The major downside I've always found is that they tie you directly to ASP.NET.

The way I went for a recent project was defining a couple of interfaces that are part of the service layer (NOTE: I simplified these quite a bit - but you could easily add to them):

public interface IAuthenticationService
{
    bool Login(string username, string password);
    void Logout(User user);
}

public interface IAuthorizationService
{
    bool Authorize(User user, Roles requiredRoles);
}

Then your users could have a Roles enum:

public enum Roles
{
    Accounting = 1,
    Scheduling = 2,
    Prescriptions = 4
    // What ever else you need to define here.
    // Notice all powers of 2 so we can OR them to combine role permissions.
}

public class User
{
    bool IsAdministrator { get; set; }
    Roles Permissions { get; set; }
}

For your IAuthenticationService, you could have a base implementation that does standard password checking and then you could have a FormsAuthenticationService that does a little bit more such as setting the cookie etc. For your AuthorizationService, you'd need something like this:

public class AuthorizationService : IAuthorizationService
{
    public bool Authorize(User userSession, Roles requiredRoles)
    {
        if (userSession.IsAdministrator)
        {
            return true;
        }
        else
        {
            // Check if the roles enum has the specific role bit set.
            return (requiredRoles & user.Roles) == requiredRoles;
        }
    }
}

On top of these base services, you could easily add services to reset passwords etc.

Since you're using MVC, you could do authorization at the action level using an ActionFilter:

public class RequirePermissionFilter : IAuthorizationFilter
{
    private readonly IAuthorizationService authorizationService;
    private readonly Roles permissions;

    public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
    {
        this.authorizationService = authorizationService;
        this.permissions = requiredRoles;
        this.isAdministrator = isAdministrator;
    }

    private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
    {
        return this.authorizationService ?? new FormsAuthorizationService(httpContext);
    }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
        // Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
        var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];

        var success = authSvc.Authorize(userSession, this.permissions);

        if (success)
        {
            // Since authorization is performed at the action level, the authorization code runs
            // after the output caching module. In the worst case this could allow an authorized user
            // to cause the page to be cached, then an unauthorized user would later be served the
            // cached page. We work around this by telling proxies not to cache the sensitive page,
            // then we hook our custom authorization code into the caching mechanism so that we have
            // the final say on whether or not a page should be served from the cache.
            var cache = filterContext.HttpContext.Response.Cache;
            cache.SetProxyMaxAge(new TimeSpan(0));
            cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
            {
                validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
            }, null);
        }
        else
        {
            this.HandleUnauthorizedRequest(filterContext);
        }
    }

    private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        // Ajax requests will return status code 500 because we don't want to return the result of the
        // redirect to the login page.
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new HttpStatusCodeResult(500);
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        var authSvc = this.CreateAuthorizationService(httpContext);
        var userSession = (User)httpContext.Session["CurrentUser"];

        var success = authSvc.Authorize(userSession, this.permissions);

        if (success)
        {
            return HttpValidationStatus.Valid;
        }
        else
        {
            return HttpValidationStatus.IgnoreThisRequest;
        }
    }
}

Which you can then decorate on your controller actions:

[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
   // ...
}

The advantage of this approach is you can also use dependency injection and an IoC container to wire things up. Also, you can use it across multiple applications (not just your ASP.NET one). You would use your ORM to define the appropriate schema.

If you need more details around the FormsAuthorization/Authentication services or where to go from here, let me know.

EDIT: To add "security trimming", you could do it with an HtmlHelper. This probably needs a little more... but you get the idea.

public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
    var authorizationService = new FormsAuthorizationService();
    var user = (User)HttpContext.Current.Session["CurrentUser"];
    return authorizationService.Authorize(user, requiredRoles);
}

And then inside your view (using Razor syntax here):

@if(Html.SecurityTrim(Roles.Accounting))
{
    <span>Only for accounting</span>
}

EDIT: The UserSession would look something like this:

public class UserSession
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public bool IsAdministrator { get; set; }
    public Roles GetRoles()
    {
         // make the call to the database or whatever here.
         // or just turn this into a property.
    }
}

This way, we don't expose the password hash and all other details inside the session of the current user since they're really not needed for the user's session lifetime.

Solution 2

I have implemented a role provider based on @TheCloudlessSky post here. There are few things that I thought I can add and share what I have done. First if you want to use the RequirepPermission class for your action filters as an attribute you need to implement ActionFilterAttribute class for RequirepPermission class.

Interface classes IAuthenticationService and IAuthorizationService

public interface IAuthenticationService
{
    void SignIn(string userName, bool createPersistentCookie);
    void SignOut();
}

public interface IAuthorizationService
{
    bool Authorize(UserSession user, string[] requiredRoles);
}

FormsAuthenticationService class

/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{

    public void SignIn(string userName, bool createPersistentCookie)
    {
        if (String.IsNullOrEmpty(userName)) throw new ArgumentException(@"Value cannot be null or empty.", "userName");

        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }

    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

UserSession calss

public class UserSession
{
    public string UserName { get; set; }
    public IEnumerable<string> UserRoles { get; set; }
}

Another point is FormsAuthorizationServiceclass and how we can assign a user to the httpContext.Session["CurrentUser"]. My Approach in this situation is to create a new instance of userSession class and directly assign the user from httpContext.User.Identity.Name to the userSession variable as you can see in FormsAuthorizationService class.

[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
    #region Fields

    private readonly IAuthorizationService _authorizationService;
    private readonly string[] _permissions;

    #endregion

    #region Constructors

    public RequirePermissionAttribute(string requiredRoles)
    {
        _permissions = requiredRoles.Trim().Split(',').ToArray();
        _authorizationService = null;
    }

    #endregion

    #region Methods

    private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
    {
        return _authorizationService ?? new FormsAuthorizationService(httpContext);
    }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var authSvc = CreateAuthorizationService(filterContext.HttpContext);
        // Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
        if (filterContext.HttpContext.Session == null) return;
        if (filterContext.HttpContext.Request == null) return;
        var success = false;
        if (filterContext.HttpContext.Session["__Roles"] != null)
        {
            var rolesSession = filterContext.HttpContext.Session["__Roles"];
            var roles = rolesSession.ToString().Trim().Split(',').ToList();
            var userSession = new UserSession
            {
                UserName = filterContext.HttpContext.User.Identity.Name,
                UserRoles = roles
            };
            success = authSvc.Authorize(userSession, _permissions);
        }
        if (success)
            {
                // Since authorization is performed at the action level, the authorization code runs
                // after the output caching module. In the worst case this could allow an authorized user
                // to cause the page to be cached, then an unauthorized user would later be served the
                // cached page. We work around this by telling proxies not to cache the sensitive page,
                // then we hook our custom authorization code into the caching mechanism so that we have
                // the final say on whether or not a page should be served from the cache.
                var cache = filterContext.HttpContext.Response.Cache;
                cache.SetProxyMaxAge(new TimeSpan(0));
                cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
                                                {
                                                    validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
                                                }, null);
            }
            else
            {
                HandleUnauthorizedRequest(filterContext);
            }
    }

    private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        // Ajax requests will return status code 500 because we don't want to return the result of the
        // redirect to the login page.
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new HttpStatusCodeResult(500);
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        var authSvc = CreateAuthorizationService(httpContext);
        if (httpContext.Session != null)
        {
            var success = false;
            if (httpContext.Session["__Roles"] != null)
            {
                var rolesSession = httpContext.Session["__Roles"];
                var roles = rolesSession.ToString().Trim().Split(',').ToList();
                var userSession = new UserSession
                {
                    UserName = httpContext.User.Identity.Name,
                    UserRoles = roles
                };
                success = authSvc.Authorize(userSession, _permissions);
            }
            return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
        }
        return 0;
    }

    #endregion
}

internal class FormsAuthorizationService : IAuthorizationService
{
    private readonly HttpContextBase _httpContext;

    public FormsAuthorizationService(HttpContextBase httpContext)
    {
        _httpContext = httpContext;
    }

    public bool Authorize(UserSession userSession, string[] requiredRoles)
    {
        return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
    }
}

then in your controller after the user is authenticated you can get roles from the database and assign it to the roles session:

var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
   ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);

After the user is logged out of the system you can clear the session

FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");

The caveat in this model is that, when the user is signed into the system, if a role is assigned to the user, authorization doesn't work unless he logs out and logs back in the system.

Another thing is that there is no need to have a separate class for roles, since we can get roles directly from database and set it into roles session in a controller.

After you are done with implementing all these codes one last step is to bind this attribute to your methods in your controller:

[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}

Solution 3

If you use Castle Windsor Dependency Injection you can inject lists of RoleProviders that can be used to ascertain user rights from any source you choose to implement.

http://ivida.co.uk/2011/05/18/mvc-getting-user-roles-from-multiple-sources-register-and-resolve-arrays-of-dependencis-using-the-fluent-api/

Share:
16,464

Related videos on Youtube

ebb
Author by

ebb

Updated on September 10, 2020

Comments

  • ebb
    ebb over 3 years

    I'm trying to avoid the use of the Role Provider and Membership Provider since its way too clumsy in my opinion, and therefore I'm trying to making my own "version" which is less clumsy and more manageable/flexible. Now is my question.. is there an alternative to the Role Provider which is decent? (I know that I can do custom Role provier, membership provider etc.)

    By more manageable/flexible I mean that I'm limited to use the Roles static class and not implement directly into my service layer which interact with the database context, instead I'm bound to use the Roles static class which has its own database context etc, also the table names is awful..

    Thanks in advance.

    • Matti Virkkunen
      Matti Virkkunen over 13 years
      I'm... not quite sure what "UnitOfWork" has to do with user access rights (roles). Isn't that thing more related to transactions than authorization?
    • ebb
      ebb over 13 years
      @Matti Virkkunen - True, forget that part :)
    • Matti Virkkunen
      Matti Virkkunen over 13 years
      Could you elaborate on what you mean by "more manageable/flexible"? Currently it seems you're not even sure about what you want.
    • ebb
      ebb over 13 years
      @Matti Virkkunen - Post updated.
    • Shawn Mclean
      Shawn Mclean over 13 years
      I have this same problem. Dependency injection cant even inject the service layer in the provider because the provider is executed before my DI even get a chance to inject.
    • TheCloudlessSky
      TheCloudlessSky over 13 years
      @Shawn - See my response. It would easily allow you to inject the appropriate authentication/authorization services into other services.
    • EBarr
      EBarr almost 13 years
      +1 for saying providers are clumsy --they feel like the result of a hack-a-ton gone awry.
    • Jagd
      Jagd almost 10 years
      Clumsy indeed! It's a half-baked pile of you-know-what. I've met some developers who swear by the .NET membership provider, and I feel bad for them because they're living in such a small, little world.
  • ebb
    ebb over 13 years
    @Matti Virkkunen - What I'm trying to is to make the Role Provider and Membership Provider a part of my ORM mappings, since it will allow me more flexibility.
  • Matti Virkkunen
    Matti Virkkunen over 13 years
    @ebb: You're being vague again. What is the concrete thing you want to do? You're free to call any ORM methods from within your provider.
  • ebb
    ebb over 13 years
    @Matti Virkkunen, At the moment I have a custom service called "UserService.cs" which dosent interact with the Membership Provider anyhow, but just got simple and stupid logic to create/get/delete users. What I'm trying to is to do the same with Role Provider... In theory its possible however I'm going into a wall when it comes to check whether a user is in a certian role since the IPrincipal User.IsInRole() wont know the users roles?
  • Matti Virkkunen
    Matti Virkkunen over 13 years
    @ebb: I'd imagine you can make IsInRole work by implementing your own RoleProvider, since it has an IsUserInRole method in it for you to override.
  • ebb
    ebb over 13 years
    @Matti Virkkunen, yea - however as I said earlier then I'm trying to avoid the use of RoleProvier since its just... too clumsy.
  • Matti Virkkunen
    Matti Virkkunen over 13 years
    @ebb: You're still failing to explain what "too clumsy" is supposed to mean.
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @Matti - I'm with @ebb on this one. The major disadvantage I've always found with the RoleProviders is that they tie you to ASP.NET for authentication/authorization. Also - a major code-smell for me is having lots of NotImplemented exceptions lying around for a custom provider.
  • ebb
    ebb over 13 years
    @Matti Virkkunen, Bound to a table with a weird name, and you'll have to define even more stuff in the web.config to enable the roleprovider and you're bound to only use RoleProviders it looks like.. so thats one more for the list. But as @TheCloudlessSky mentioned then I could just implement a custom provider which only holds logic for the IsUserInRole() method and then just NotImplemented Exceptions for the rest... But thats just odd.
  • ebb
    ebb over 13 years
    Nothing less than perfect! Just a curious question: How would you check whether a user is in a role in the view? (To render different menu items for a regular User and an Administrator)?
  • Shawn Mclean
    Shawn Mclean over 13 years
    This makes more sense because the only reason I use rolesprovider is to get the attribute [Authorize(Roles="stuff")]
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @Shawn - If you look at my question history, I have an ongoing question where I'm trying to solve the authorize attribute issue without using roles, but granular permissions. I like using them too!
  • ebb
    ebb over 13 years
    @TheCloudlessSky, one thing that I'm worried about is to store the User in a session, along with the Roles... Take this scenario: Joe decides to remove the Role "Marketing" from Bill while he is logged in.. and since Bill have a session running with his Roles it wont update? - But of course I could just add a DB call in the FormsAuthorizationService() that checks whether the user is in the role.
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - Yeah it's a rare case but valid. You could either inform the user "Permission changes will not take affect until the user logs in again", or always load the permissions every time they authorize (more hits to the database though).
  • ebb
    ebb over 13 years
    @TheCloudlessSky, Yea... guess it depends on the scenario. Another thing: How/Where do you set the session for HttpContext.Current.Session["User"]?
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - Also another thing that I didn't mention (since it added complexity to the post) would maybe be storing a UserSession instead of the actual User in the session. You could then have a GetRoles() that would make the database call.
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - You'd set that inside the Login on the FormsAuthenticationSerivce. It's the same place where you'd set the cookie. If you need me to provide this implementation, I can write one up.
  • ebb
    ebb over 13 years
    @TheCloudlessSky, Think I got it :) - However the one about storing a UserSession instead of the actual User in the session - Could you please elaborate that? (How to do it)
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - @ebb - Well inside your authentication service, you would get the User from the unit of work/repository. To me, it feels wrong to store the actual user entity inside the session, so I transform it into a UserSession (where it doesn't keep the password etc). It just knows what it needs to know. So where you see the Session["CurrentUser"], you would set/get a UserSession instead of a User. See my edit above. Make sense?
  • ebb
    ebb over 13 years
    @TheCloudlessSky, yea makes perfectly sense... what you do is only to store the nessecary and nothing else. - Think I'll get something to eat and give it a shot :) - Thank you very much for your time and help, really appreciate it.
  • ebb
    ebb over 13 years
    @TheCloudlessSky - Just a last question (if you still around) - Do I still need to set a cookie in my FormsAuthenticationService, or is the UserSession a replacement for it?
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - You'd still need the cookies in FormsAuthenticationService since that would be it's job. UserSession is just like User, but less properties.
  • ebb
    ebb over 13 years
    @TheCloudlessSky, ah.. so the UserSession is just for checking roles etc. - Thanks once again :)
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - Yeah exactly. It's also what you'd use in your masterpage/layout to display the "current user's name". Or if you want to grab the "current user's id", you'd use its UserId property. Cheers.
  • ebb
    ebb over 13 years
    @TheCloudlessSky - Guess this is my second last question ;p - How do I prevent the session from running out before the cookie dies?
  • TheCloudlessSky
    TheCloudlessSky over 13 years
    @ebb - There are several resources available for session timeouts: justgeeks.blogspot.com/2008/07/aspnet-session-timeouts.html, aspalliance.com/520_Detecting_ASPNET_Session_Timeouts.2 are two from a quick Google search. They provide the details needed to start to understand the session timeouts. If you're getting stuck with actual code, post a new question with your code so I and others can take a look (if you want to notify me - use "@TheCloudlessSky" in a comment or the post).
  • ebb
    ebb over 13 years
    @TheCloudlessSky - I've created a new post about constructor injection in actionfilters if you have a moment or two :) The post: stackoverflow.com/questions/4839293/…
  • Daniel Harvey
    Daniel Harvey over 12 years
    How do you get access to your Roles in the Views? It keeps pulling in System.Web.Security, even after adding my own namespace to the page referenced assemblies.
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    @DanielHarvey - I would do something like @using Namespace.To.Roles at the top of the view or reference the whole namespace to the role @NS.To.Security.Roles.Accounting.
  • Ryan
    Ryan about 12 years
    @TheCloudlessSky - So do you still call codeFormsAuthentication.SetAuthCookie(); in FormsAuthenticationSerivce and also store UserSession in the Session? So there will be HttpContext.User as well as your session object? I really want to stay away from using custom IIdentity :S
  • TheCloudlessSky
    TheCloudlessSky about 12 years
    @Ryan - That's correct. You could store the custom UserSession when the user logs in - this storage mechanism could be HttpContext.Session or some other key/value store (Redis etc..). You shouldn't have to implement a custom IIdentity with this solution.
  • Ryan
    Ryan about 12 years
    @TheCloudlessSky - Right. However when I do SetAuthCookie that will automatically create the HttpContext.User for me. Is there a way to do it without setting the cookie or is that impossible? I just don't see the need to have a HttpContext.User as well as the Session["User"]
  • TheCloudlessSky
    TheCloudlessSky about 12 years
    @Ryan - Doing Session["User"] is for the UserSession object that has more details than the IIdentity. So you'll only need to SetAuthCookie and then set the Session["User"]. The approach in this post leverages WebForms security (it's a tried-and-true - don't write your own authentication scheme), which is why SetAuthCookie is required. HttpContext.User will still return the IIdentity, but it will only contain the username.. nothing more.
  • Ryan
    Ryan about 12 years
    @TheCloudlessSky - Thanks a lot for confirming that for me :)
  • TheCloudlessSky
    TheCloudlessSky about 12 years
    @Ryan - No problem - if you have any more questions, don't hesitate to ask!
  • Ryan
    Ryan about 12 years
    @TheCloudlessSky - So I've got this running but I'm getting in to cases where the cookie will still be available so the user is logged in but the session will be cleared... Would this be fixed storing the session in the database or is there something else I can do?
  • TheCloudlessSky
    TheCloudlessSky about 12 years
    @Ryan - It depends on how it's being cleared. Is the session being loaded because the session has expired or because the session has been cleared on server side (aka the session is stored in memory and the AppPool was reset by a recompile). The simplest solution would be to just reload the session variable from the DB if the user is still logged in. However, you could easily use a different session provider (aka the DB or another in memory store) that isn't affected by AppPool resets.
  • Ryan
    Ryan about 12 years
    @TheCloudlessSky - Well both actually I will have to check out using a db for the session store or something else I don't want to litter my code with forwards to get the session from the db everywhere
  • TheCloudlessSky
    TheCloudlessSky about 12 years
    @Ryan - Sorry for the delay - I didn't see that you had responded. You wouldn't actually reload in your controller code - you'd do it inside of the attribute for the authorization. Before it loads, you could check to see if the session has already expired, and if so requery the DB. Therefore, you only put it in one place.