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);