As per the post Passive Attributes, the DI-friendly solution is to separate the AuthorizeAttribute
into 2 parts:
- An attribute that contains no behavior to flag your controllers and action methods with.
- A DI-friendly class that implements IAuthorizationFilter and contains the desired behavior.
For our purposes, we just inherit AuthorizeAttribute
to take advantage of some of its built in functionality.
Note that if you take this approach, it doesn’t make much sense to use property injection for your database dependencies. Constructor injection is always a better choice, anyway.
ClaimsIdentityAuthorizeAttribute
First of all, we have our attribute that has no behavior to flag our controllers and actions with. We add a little bit of smartness to parse the permissions out into an array so that doesn’t have to be done on every authorization check.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class ClaimsAuthorizeAccountAccess : Attribute
{
private readonly string[] _permissionsSplit;
public ClaimsAuthorizeAccountAccess(string permissions)
{
_permissionsSplit = SplitString(value);
}
internal string[] PermissionsSplit
{
get { return this._permissionsSplit; }
}
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
ClaimsIdentityAuthorizationFilter
Next, we have our authorization filter which will act as a global filter.
We add a WhiteListMode
which is true by default because that is the recommended way to configure security (controllers and actions require a login unless they are given an AllowAnonymousAttribute
). Fortunately, the framework for that is built into AuthorizeAttribute
so we just use it as a flag whether or not to check globally.
We also add an extension point where our custom authorization service can be injected. The 2 most likely things to change are:
- The test to determine whether the action is authorized.
- The action to take when the user is not authorized.
So those are the things that we add to our service. You could refactor this into 2 separate services, if desired.
public class ClaimsIdentityAuthorizationFilter : AuthorizeAttribute
{
private readonly IAuthorizationService _authorizationService;
private string _permissions;
private string[] _permissionsSplit = new string[0];
private bool _whiteListMode = true;
public ClaimsIdentityAuthorizationFilter(IAuthorizationService authorizationService)
{
if (authorizationService == null)
throw new ArgumentNullException("authorizationService");
this._authorizationService = authorizationService;
}
// Hide users and roles, since we aren't using them.
[Obsolete("Not applicable in this class.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
new public string Roles { get; set; }
[Obsolete("Not applicable in this class.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
new public string Users { get; set; }
public string Permissions
{
get
{
return (this._permissions ?? string.Empty);
}
set
{
this._permissions = value;
this._permissionsSplit = SplitString(value);
}
}
public bool WhiteListMode
{
get { return this._whiteListMode; }
set { this._whiteListMode = value; }
}
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
private ClaimsAuthorizeAccountAccess GetClaimsAuthorizeAccountAccess(ActionDescriptor actionDescriptor)
{
ClaimsAuthorizeAccountAccess result = null;
// Check if the attribute exists on the action method
result = (ClaimsAuthorizeAccountAccess)actionDescriptor
.GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
.SingleOrDefault();
if (result != null)
{
return result;
}
// Check if the attribute exists on the controller
result = (ClaimsAuthorizeAccountAccess)actionDescriptor
.ControllerDescriptor
.GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
.SingleOrDefault();
return result;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
if (actionDescriptor != null)
{
var authorizeAttribute = this.GetClaimsAuthorizeAccountAccess(actionDescriptor);
// If the authorization attribute exists
if (authorizeAttribute != null)
{
// Run the authorization based on the attribute
return this._authorizationService.HasPermission(
httpContext,
authorizeAttribute.PermissionsSplit);
}
else if (this.WhiteListMode)
{
// Run the global authorization
return this._authorizationService.HasPermission(
httpContext,
this._permissionsSplit);
}
}
return true;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
// Pass the current action descriptor to the AuthorizeCore
// method on the same thread by using HttpContext.Items
filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
base.OnAuthorization(filterContext);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = this._authorizationService.GetUnauthorizedHandler(filterContext);
}
}
IAuthorizationService
public interface IAuthorizationService
{
bool HasPermission(HttpContextBase httpContext, string[] permissions);
ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext);
}
ClaimsIdentityAuthorizationService
So now we do the advanced customization to support claims. We separate this so there is a seam we can use to inject another instance if the business logic changes in the future.
public class ClaimsIdentityAuthorizationService : IAuthorizationService
{
private IAccountRepository _accountRepository { get; set; }
public ClaimsIdentityAuthorizationService(IAccountRepository accountRepository)
{
if (accountRepository == null)
throw new ArgumentNullException("accountRepository");
_accountRepository = accountRepository;
}
public bool HasPermission(HttpContextBase httpContext, string[] permissions)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
if (!user.IsInRole("Account Owner"))
{
ClaimsIdentity claimsIdentity = (ClaimsIdentity)user.Identity;
List<AccountLinkPermissionDTO> accountLinkPermissions = new List<AccountLinkPermissionDTO>();
int accountOwnerID = 0;
Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == "AccountOwnerID").Select(c => c.Value).SingleOrDefault(), out accountOwnerID);
int guestID = 0;
Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.Sid).Select(c => c.Value).SingleOrDefault(), out guestID);
//NULL
accountLinkPermissions = _accountRepository.GetAccountLinkPermissions(accountOwnerID, guestID);
if (accountLinkPermissions != null)
{
List<string> accountLinkPermissionsToString = accountLinkPermissions.Select(m => m.Permission.Name).ToList();
int hits = accountLinkPermissionsToString.Where(m => permissions.Contains(m)).Count();
if (hits == 0)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
public ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext)
{
//Guest doesnt have right permissions
return new RedirectToRouteResult(
new RouteValueDictionary {
{ "action", "AccessDenied" },
{ "controller", "Account" }
});
}
}
Usage
Register your filter globally and inject its dependencies with your container.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters, IUnityContainer container)
{
filters.Add(new HandleErrorAttribute());
filters.Add(container.Resolve<IAuthorizationFilter>());
}
}
NOTE: If you need any of the filter’s dependencies to have a lifetime shorter than singleton, you will need to use a
GlobalFilterProvider
as in this answer.
Startup
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
var container = Bootstrapper.Initialise();
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters, container);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
Bootstrapper
public static class Bootstrapper
{
public static IUnityContainer Initialise()
{
var container = BuildUnityContainer();
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
return container;
}
private static IUnityContainer BuildUnityContainer()
{
var container = new UnityContainer();
container.RegisterType<IAccountRepository, AccountRepository>();
container.RegisterType<IAdministrationRepository, AdministrationRepository>();
container.RegisterType<IUploadDirectlyRepository, UploadDirectlyRepository>();
container.RegisterType<IUserRepository, UserRepository>();
container.RegisterType<INewsRepository, NewsRepository>();
container.RegisterType<IContactRepository, ContactRepository>();
// Register the types for the authorization filter
container.RegisterType<IAuthorizationFilter, ClaimsIdentityAuthorizationFilter>(
// Not sure whether you want white list or black list
// but here is where it is set.
new InjectionProperty("WhiteListMode", true),
// For white list security, you can also set the default
// permissions that every action gets if it is not overridden.
new InjectionProperty("Permissions", "read"));
container.RegisterType<IAuthorizationService, ClaimsIdentityAuthorizationService>();
// register all your components with the container here
// it is NOT necessary to register your controllers
// e.g. container.RegisterType<ITestService, TestService>();
RegisterTypes(container);
return container;
}
public static void RegisterTypes(IUnityContainer container)
{
}
}
And then in your controller, for black list security, you will need to decorate every action (or controller) to lock it down.
public class HomeController : Controller
{
// This is not secured at all
public ActionResult Index()
{
return View();
}
[ClaimsAuthorizeAccountAccess("read")]
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
[ClaimsAuthorizeAccountAccess("read,edit")]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
For white list security, you only need to decorate the actions that everyone has access to with AllowAnonymous
or add a ClaimsIdentityAuthorizeAttribute
with more or less restrictive permissions than the global or controller level.
public class HomeController : Controller
{
// This is not secured at all
[AllowAnonymous]
public ActionResult Index()
{
return View();
}
// This is secured by ClaimsAuthorizeAccountAccess (read permission)
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
[ClaimsAuthorizeAccountAccess("read,edit")]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}