Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a AuthorizeAttribute - what do I need to know?

Here are my requirements:

  • I'll be adding users to N amount of roles; defined in a database.

  • I need to protect each controller action with my authorize attribute.

For example, the web application would check if the logged in user belongs to either of these two roles and if they do, I let them in. How can I tell the Authorize attribute to fetch the user roles from a database table I choose?

 [Authorize(Roles = "Admin, Technician")]
 public ActionResult Edit(int id)
 {
     return View();
 }

I've tried Googling for many different pages, but none seem to fit in with what I need and are overly complicated.

If the official documentation has something I'd love to find it as well, as I didn't see anything I could use.

Any suggestions?

For example, this question has a very clean looking answer, but I don't know if it's complete or missing something important.

ASP.NET MVC3 Role and Permission Management -> With Runtime Permission Assignment


Edit

It seems that what I'm actually looking for is creating a custom role provider, correct? Do I need to implement this class and use it as my Role provider? I'm fairly new at this, any thoughts?

http://msdn.microsoft.com/en-us/library/8fw7xh74.aspx

like image 963
Only Bolivian Here Avatar asked Jan 12 '12 03:01

Only Bolivian Here


People also ask

How do I create a custom authorization filter in Web API?

To implement a custom authorization filter, we need to create a class that derives either AuthorizeAttribute , AuthorizationFilterAttribute , or IAuthorizationFilter . AuthorizeAttribute : An action is authorized based on the current user and the user's roles.

When should we use Authorize attribute?

This attribute is useful when you want to use the Authorize attribute on a controller to protect all of the actions inside, but then there is this single action or one or two actions that you want to unprotect and allow anonymous users to reach that specific action.

Under Which method should a policy be registered for it to be a part of authorization service?

First, you have to register your policy in the ConfigureServices() method of the Startup class, as part of the authorization service configuration.

How does the Authorize attribute work?

Web API provides a built-in authorization filter, AuthorizeAttribute. This filter checks whether the user is authenticated. If not, it returns HTTP status code 401 (Unauthorized), without invoking the action. We can apply the filter globally, at the controller level, or at the level of individual actions.


2 Answers

I've been going through pretty much the same scenario the past couple of weeks so this might help someone else in the same boat. My scenario is an MVC4 application on a company intranet with users stored in Active Directory. This allows for Windows authentication giving single sign-on so no need for Forms authentication. Roles are stored in an Oracle database. I have 3 roles:

  • Readonly: All users need to be a member of this to access the application
  • User: Create new resords
  • Admin: Edit and delete records

I decided to use the asp.net role provider api to create my own AccountRoleProvider. So far I only need to use 2 methods in this, GetRolesForUser and IsUserInRole:

public class AccountRoleProvider : RoleProvider // System.Web.Security.RoleProvider
{
    private readonly IAccountRepository _accountRepository;

    public AccountRoleProvider(IAccountRepository accountRepository)
    {
        this._accountRepository = accountRepository;
    }

    public AccountRoleProvider() : this (new AccountRepository())
    {}

    public override string[] GetRolesForUser(string user521)
    {
        var userRoles = this._accountRepository.GetRoles(user521).ToArray();

        return userRoles;
    }

    public override bool IsUserInRole(string username, string roleName)
    {
        var userRoles = this.GetRolesForUser(username);

        return Utils.IndexOfString(userRoles, roleName) >= 0;
    }
}

I updated the web.config to use my role provider:

<authentication mode="Windows" />
<roleManager enabled="true" defaultProvider="AccountRoleProvider">
  <providers>
    <clear/>
    <add name="AccountRoleProvider"
         type="MyApp.Infrastructure.AccountRoleProvider" />
  </providers>
</roleManager>

Then I created 2 custom attributes from AuthorizeAttribute, ReadOnlyAuthorize and CustomAuthorize.

ReadonlyAuthorize:

public class ReadonlyAuthorize : AuthorizeAttribute
{
    private IAccountRepository _accountRepository;

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var user = httpContext.User;
        this._accountRepository = new AccountRepository();

        if (!user.Identity.IsAuthenticated)
        {
            return false;
        }

        // Get roles for current user
        var roles = this._accountRepository.GetRoles(user.Identity.Name);

        if (!roles.Contains("readonly"))
        {
            return false;
        }

        return base.AuthorizeCore(httpContext);
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (filterContext.HttpContext.User.Identity.IsAuthenticated && filterContext.Result is HttpUnauthorizedResult)
        {
            filterContext.Result = new ViewResult { ViewName = "AccessDenied" };
        }
    }
}

CustomAuthorize:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    public string RedirectActionName { get; set; }
    public string RedirectControllerName { get; set; }
    private IAccountRepository _accountRepository;

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var user = httpContext.User;
        this._accountRepository = new AccountRepository();
        var accessAllowed = false;

        // Get the roles passed in with the (Roles = "...") on the attribute
        var allowedRoles = this.Roles.Split(',');

        if (!user.Identity.IsAuthenticated)
        {
            return false;
        }

        // Get roles for current user
        var roles = this._accountRepository.GetRoles(user.Identity.Name);

        foreach (var allowedRole in allowedRoles)
        {
            if (roles.Contains(allowedRole))
            {
                accessAllowed = true;
            }
        }

        if (!accessAllowed)
        {
            return false;
        }

        return base.AuthorizeCore(httpContext);
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (filterContext.HttpContext.User.Identity.IsAuthenticated && filterContext.Result is HttpUnauthorizedResult)
        {   
            var values = new RouteValueDictionary(new
            {
                action = this.RedirectActionName == string.Empty ? "AccessDenied" : this.RedirectActionName,
                controller = this.RedirectControllerName == string.Empty ? "Home" : this.RedirectControllerName
            });

            filterContext.Result = new RedirectToRouteResult(values);
        }
    }
}

The reason for 2 different attributes is that I use one for the Readonly role that all users must be a member of in order to access the app. I can add this in the RegisterGlobalFilters method in Global.asax which means it's applied automatically to every Controller:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new ReadonlyAuthorize());
}

Then in the CustomAuthorize I can take a more granular approach and specify the roles that I want and apply to a Controller or an individual Action e.g. below I can restrict access to the Delete method to users in the Admin role:

[AccessDeniedAuthorize(RedirectActionName = "AccessDenied", RedirectControllerName = "Home", Roles = "Admin")]
public ActionResult Delete(int id = 0)
{
    var batch = myDBContext.Batches.Find(id);
    if (batch == null)
    {
        return HttpNotFound();
    }

    return View(batch);
}

There are further steps I need to take such as updating the User object with the roles the current user is a member of. This will retrieve the roles for the User once instead of every time in my custom attributes, and also utilise User.IsInRole. Something like this should be possible in Application_AuthenticateRequest in Gloal.asax:

var roles = "get roles for this user from respository";

if (Context.User != null)
    Context.User = new GenericPrincipal(Context.User.Identity, roles);
like image 102
Ciarán Bruen Avatar answered Sep 28 '22 04:09

Ciarán Bruen


There are a bunch of ways to handle this. Darin's method and blowdarts (both very skilled individuals - one of them is a security author as well) are decent in the link you provided.

One thing to watch out for is the cache. If you use server side outputcache caching, you may inadvertently cache something for one user that gets returned to another user. Please see:

OutputCache and Authorize filters in MVC3

and

Why can't I combine [Authorize] and [OutputCache] attributes when using Azure cache (.NET MVC3 app)?

and

MVC Custom Authentication, Authorization, and Roles Implementation

for additional info on that and how to handle caching if you are using an authorize attribute.

like image 40
Adam Tuliper Avatar answered Sep 28 '22 03:09

Adam Tuliper