47

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.

TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
ebb
  • 9,297
  • 18
  • 72
  • 123
  • 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? – Matti Virkkunen Jan 29 '11 at 13:29
  • @Matti Virkkunen - True, forget that part :) – ebb Jan 29 '11 at 13:33
  • 2
    Could you elaborate on what you mean by "more manageable/flexible"? Currently it seems you're not even sure about what you want. – Matti Virkkunen Jan 29 '11 at 13:43
  • @Matti Virkkunen - Post updated. – ebb Jan 29 '11 at 13:53
  • 2
    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. – Shawn Mclean Jan 29 '11 at 15:09
  • @Shawn - See my response. It would easily allow you to inject the appropriate authentication/authorization services into other services. – TheCloudlessSky Jan 29 '11 at 15:16
  • 7
    +1 for saying providers are clumsy --they feel like the result of a hack-a-ton gone awry. – EBarr May 24 '11 at 01:33
  • 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. – Jagd May 14 '14 at 20:15

5 Answers5

89

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.

TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
  • 3
    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)? – ebb Jan 29 '11 at 15:18
  • This makes more sense because the only reason I use rolesprovider is to get the attribute `[Authorize(Roles="stuff")]` – Shawn Mclean Jan 29 '11 at 15:20
  • @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! – TheCloudlessSky Jan 29 '11 at 15:26
  • @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. – ebb Jan 29 '11 at 15:30
  • 1
    @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). – TheCloudlessSky Jan 29 '11 at 15:34
  • @TheCloudlessSky, Yea... guess it depends on the scenario. Another thing: How/Where do you set the session for HttpContext.Current.Session["User"]? – ebb Jan 29 '11 at 15:38
  • @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 Jan 29 '11 at 15:38
  • @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. – TheCloudlessSky Jan 29 '11 at 15:39
  • @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) – ebb Jan 29 '11 at 15:43
  • 1
    @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? – TheCloudlessSky Jan 29 '11 at 15:53
  • @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 Jan 29 '11 at 15:59
  • @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? – ebb Jan 29 '11 at 16:39
  • 1
    @ebb - You'd still need the cookies in FormsAuthenticationService since that would be it's job. UserSession is just like User, but less properties. – TheCloudlessSky Jan 29 '11 at 16:55
  • @TheCloudlessSky, ah.. so the UserSession is just for checking roles etc. - Thanks once again :) – ebb Jan 29 '11 at 16:59
  • @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. – TheCloudlessSky Jan 29 '11 at 17:00
  • @TheCloudlessSky - Guess this is my second last question ;p - How do I prevent the session from running out before the cookie dies? – ebb Jan 29 '11 at 17:23
  • @ebb - There are several resources available for session timeouts: http://justgeeks.blogspot.com/2008/07/aspnet-session-timeouts.html, http://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). – TheCloudlessSky Jan 29 '11 at 20:21
  • @TheCloudlessSky - I've created a new post about constructor injection in actionfilters if you have a moment or two :) The post: http://stackoverflow.com/questions/4839293/asp-net-mvc3-actionfilterattribute-injection – ebb Jan 29 '11 at 21:07
  • 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. – Daniel Harvey Feb 03 '12 at 22:45
  • 1
    @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`. – TheCloudlessSky Feb 04 '12 at 19:15
  • @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 – Ryan Mar 13 '12 at 03:36
  • @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. – TheCloudlessSky Mar 13 '12 at 16:05
  • @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"]` – Ryan Mar 13 '12 at 16:21
  • 1
    @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. – TheCloudlessSky Mar 13 '12 at 16:29
  • @TheCloudlessSky - Thanks a lot for confirming that for me :) – Ryan Mar 13 '12 at 17:34
  • 1
    @Ryan - No problem - if you have any more questions, don't hesitate to ask! – TheCloudlessSky Mar 13 '12 at 18:35
  • @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? – Ryan Apr 28 '12 at 06:25
  • @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. – TheCloudlessSky Apr 28 '12 at 14:42
  • @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 – Ryan Apr 28 '12 at 21:05
  • 1
    @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. – TheCloudlessSky May 03 '12 at 23:15
5

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();
}
Hamid Tavakoli
  • 4,567
  • 1
  • 33
  • 34
2

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/

ActualAl
  • 1,192
  • 10
  • 13
0

You don't need to use a static class for roles. For instance, the SqlRoleProvider allows you to define the roles in a database.

Of course, if you want to retrieve roles from your own service layer, it's not that hard to create your own role provider - there really aren't that many methods to implement.

Matti Virkkunen
  • 63,558
  • 9
  • 127
  • 159
  • 1
    @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. – ebb Jan 29 '11 at 14:00
  • 2
    @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. – Matti Virkkunen Jan 29 '11 at 14:03
  • @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? – ebb Jan 29 '11 at 14:09
  • 1
    @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. – Matti Virkkunen Jan 29 '11 at 14:12
  • @Matti Virkkunen, yea - however as I said earlier then I'm trying to avoid the use of RoleProvier since its just... too clumsy. – ebb Jan 29 '11 at 14:21
  • @ebb: You're still failing to explain what "too clumsy" is supposed to mean. – Matti Virkkunen Jan 29 '11 at 14:22
  • @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. – TheCloudlessSky Jan 29 '11 at 14:27
  • 1
    @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 Jan 29 '11 at 14:28
0

You can implement your own membership and role providers by overriding the appropriate interfaces.

If you want to start from scratch, typically these types of things are implemented as a custom http module which stores the users credentials either in the httpcontext or the session. Either way you'll probably want to set a cookie with some sort of authentication token.

Brook
  • 5,949
  • 3
  • 31
  • 45