If your web API is just used in an existing MVC application, I recommend creating your own AuthorizeAttribute
filter for your MVC and WebApi controllers; I create what I call the AuthorizeSafe filter, which is the default blacklist, so if you forget to apply the authorization attribute to the controller or method, you will be denied access (I think that by default the whitelist is unsafe).
Two attribute classes are provided for expansion; System.Web.Mvc.AuthorizeAttribute
and System.Web.Http.AuthorizeAttribute
; the first is used with MVC forms authentication, and the second also connects to forms authentication (this is very good because it means that you do not need to create a whole separate authentication architecture for authentication and API authorization). Here's what I came up with - it denies access to all default MVC controllers / actions and WebApi controllers / actions by default unless the AllowAnonymous
or AuthorizeSafe
attribute is used. First, an extension method to help with custom attributes:
public static class CustomAttributeProviderExtensions { public static List<T> GetCustomAttributes<T>(this ICustomAttributeProvider provider, bool inherit) where T : Attribute { List<T> attrs = new List<T>(); foreach (object attr in provider.GetCustomAttributes(typeof(T), false)) { if (attr is T) { attrs.Add(attr as T); } } return attrs; } }
A helper authorization class that uses AuthorizeAttribute
extensions:
public static class AuthorizeSafeHelper { public static AuthActionToTake DoSafeAuthorization(bool anyAllowAnonymousOnAction, bool anyAllowAnonymousOnController, List<AuthorizeSafeAttribute> authorizeSafeOnAction, List<AuthorizeSafeAttribute> authorizeSafeOnController, out string rolesString) { rolesString = null;
The two extension classes themselves:
public sealed class AuthorizeSafeFilter : System.Web.Mvc.AuthorizeAttribute { public override void OnAuthorization(AuthorizationContext filterContext) { if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) { throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers. Use the AuthorizeSafeAttribute with individual actions/controllers."); } string rolesString; AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization( filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0, filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0, filterContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false), filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false), out rolesString ); string rolesBackup = this.Roles; try { switch (action) { case AuthActionToTake.SkipAuthorization: return; case AuthActionToTake.NormalAuthorization: this.Roles = rolesString; base.OnAuthorization(filterContext); return; case AuthActionToTake.Unauthorized: filterContext.Result = new HttpUnauthorizedResult(); return; } } finally { this.Roles = rolesBackup; } } } public sealed class AuthorizeSafeApiFilter : System.Web.Http.AuthorizeAttribute { public override void OnAuthorization(HttpActionContext actionContext) { if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) { throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers. Use the AuthorizeSafeAttribute with individual actions/controllers."); } string rolesString; AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization( actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0, actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0, actionContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(), actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(), out rolesString ); string rolesBackup = this.Roles; try { switch (action) { case AuthActionToTake.SkipAuthorization: return; case AuthActionToTake.NormalAuthorization: this.Roles = rolesString; base.OnAuthorization(actionContext); return; case AuthActionToTake.Unauthorized: HttpRequestMessage request = actionContext.Request; actionContext.Response = request.CreateResponse(HttpStatusCode.Unauthorized); return; } } finally { this.Roles = rolesBackup; } } }
And finally, an attribute that can be applied to methods / controllers to allow users in certain roles to access them:
public class AuthorizeSafeAttribute : Attribute { public string Roles { get; set; } }
Then we register our AuthorizeSafe filters globally from Global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { // Make everything require authorization by default (whitelist approach) filters.Add(new AuthorizeSafeFilter()); } public static void RegisterWebApiFilters(HttpFilterCollection filters) { // Make everything require authorization by default (whitelist approach) filters.Add(new AuthorizeSafeApiFilter()); }
Then, to open the action, for example. anonymous access or admin access only:
public class AccountController : System.Web.Mvc.Controller { // GET: /Account/Login [AllowAnonymous] public ActionResult Login(string returnUrl) { // ... } } public class TestApiController : System.Web.Http.ApiController { // GET API/TestApi [AuthorizeSafe(Roles="Admin")] public IEnumerable<TestModel> Get() { return new TestModel[] { new TestModel { TestId = 123, TestValue = "Model for ID 123" }, new TestModel { TestId = 234, TestValue = "Model for ID 234" }, new TestModel { TestId = 345, TestValue = "Model for ID 345" } }; } }