I'm currently writing an Admin MVC 3 site, and each user only has access to certain parts of the site.
The areas of my site are the same as the user Roles, so what I would like to do is the put the AuthorizeAttribute on each area, using the area's name as the parameter in the Role.
So far I've got this to work when I'm hard coding the checking of each area, but I would like to just loop through all areas and apply the Authorize filter.
(i'm using this as my custom FilterProvider - http://www.dotnetcurry.com/ShowArticle.aspx?ID=578)
My code so far ("Gcm" is one of my areas, and is also a Role) :
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
// for all controllers, run AdminAuthorizeAttribute to make sure they're at least logged in
filters.Add(ObjectFactory.GetInstance<AdminAuthorizeAttribute>());
AdminAuthorizeAttribute gcmAuthroizeAttribute = ObjectFactory.GetInstance<AdminAuthorizeAttribute>();
gcmAuthroizeAttribute.Roles = "Gcm";
var provider = new FilterProvider();
provider.Add(
x =>
x.RouteData.DataTokens["area"] != null && x.RouteData.DataTokens["area"].ToString() == "Gcm"
? gcmAuthroizeAttribute
: null);
FilterProviders.Providers.Add(provider);
}
Does anyone know how to get all the areas of my application, so I can just loop through them, rather than hard coding each area?
Or if anyone has a better idea of how to Authorize per area, that would be appreciated to.
Thanks for your help
Saan
You could you make a base controller for each area, and put the authorize attribute over the base class. That way you can pass the area parameter in for each area's base controller.
Here is an example of a Authorize Attribute override i have created. I needed my authorize function to support to types of member ship so you might not want to get too into the inner workings of the functions, but AuthorizeCore is where the main logic occures. In my case i am checking it against a entity datacontext.
Usage:
[AjaxAuthorize(AjaxRole = "Administrators")]
public JsonResult SaveAdministrativeUser(v.... )
Code:
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
private class HttpAuthorizeFailedResult : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 403. Membership.Provider.Name == "UnitArchiveMembershipProvider"
context.HttpContext.Response.StatusCode = context.HttpContext. User.Identity is WindowsIdentity ? 401 : 403;
}
}
public string AjaxRole { get; set;}
public AjaxAuthorizeAttribute()
{
AjaxRole = "Users";
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (string.IsNullOrEmpty(MvcApplication.Config.DBSettings.Database))
{
return true;
}
//When authorize parameter is set to false, not authorization should be performed.
UnitArchiveData db = DataContextFactory.GetWebRequestScopedDataContext<UnitArchiveData>(MvcApplication.Config.DBSettings.GetConnectionString());
if (httpContext.User.Identity.IsAuthenticated)
{
login_data user = db.login_datas.Where(n => n.EmailAddress == httpContext.User.Identity.Name).FirstOrDefault();
if (user != null)
{
return user.cd_login_role.RoleName == "Administrators" || user.cd_login_role.RoleName == AjaxRole;
}
}
return false;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 403 error.
filterContext.Result = new HttpAuthorizeFailedResult();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
When I was investigating a separate issue, I came across How to pass parameters to a custom ActionFilter in ASP.NET MVC 2?
That attribute example can be altered to check for the current Controller's area.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
RouteData routeData = filterContext.RouteData;
// check if user is allowed on this page
if (SessionFactory.GetSession().Contains(SessionKey.User))
{
User user = (User)SessionFactory.GetSession().Get(SessionKey.User);
string thisArea = routeData.DataTokens["area"].ToString();
// if the user doesn't have access to this area
if (!user.IsInRole(thisArea))
{
HandleUnauthorizedRequest(filterContext);
}
}
// do normal OnAuthorization checks too
base.OnAuthorization(filterContext);
}
}
I then apply my custom authorize attribute to all controllers like this in Global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
// for all controllers, run CustomAuthorizeAttribute to make sure they're at logged in and have access to area
filters.Add(ObjectFactory.GetInstance<CustomAuthorizeAttribute>());
}
Thanks for all who replied
Saan
Related
I have register a global filter to authorize requests that need a cookie but I have a controller that needs to be public so I add [AllowAnonymous] attribute to the controller methods but my filter still fires and keeps redirecting. I'm not sure the best way to fix this issue.
Do I need to modify my onauthorization method to look for the [AllowAnonymous] attribute?
public class CookieAuthFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies.Get("token");
if (cookie == null)
{
filterContext.Result = new RedirectResult("/Home/Index");
}
}
}
Do I need to modify my onauthorization method to look for the [AllowAnonymous] attribute?
You could, but it would be simpler just to move your logic so the base OnAuthorize method (which contains the logic to scan for [AllowAnonymous]) is unmodified.
public class CookieAuthFilter : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.Cookies.Get("token") != null;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectResult("/Home/Index");
}
}
It is better to use AuthorizeCore to return true if the user is authorized, and use HandleUnauthorizedRequest for the redirect.
OnAuthorization also contains some additional logic to help it deal with output caching that you should leave in place.
I'm using MvcSitemapProvider for my ASP MVC 5 project.
I've implemented a custom Authorize to check if roles from sitemap are equal to users roles.
This is what it looks like:
public class CustomAuthorize : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool authorize = false;
var roles = SiteMaps.Current.CurrentNode.Roles;
foreach (var role in roles)
if (System.Web.Security.Roles.IsUserInRole(role))
authorize = true;
return authorize;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
My issue is that when using SiteMaps.Current.CurrentNode.Roles it throws a An unhandled exception of type 'System.StackOverflowException' occurred in System.Web.dll and I have no clue why. The objects are filled, nothing is null.
Why does this happen? Right now I'm clueless about how to get this to work as a simple currentNode doesn't work...
The default implementation of AuthorizeAttribute is all you need to interact with MvcSiteMapProvider. AuthorizeAttribute already supports roles.
// Only users in the "Manager" role have access to all actions on this controller
[Authorize(Roles = "Manager")]
public class AccountController : Controller
{
public ActionResult Manage()
{
return View();
}
public ActionResult ChangePassword()
{
return View();
}
}
The only thing you need to do is enable security trimming. See the security trimming documentation.
I think the call to current node is generating another request for the page which will again invoke your authorization filter. In other words, this code is creating an infinite loop of calls to itself none of which ever return which is why the call stack is overflowing.
I would store the roles you want to check for in another way.
So I'm setting up my permission for an mvc website. And I'm doing a role based permission, having actions in a controller would require different Roles depending on the purpose of the action.
I know that the most recommended would be authorizeattribute (as i want the roles cached) but is it possible to have the same with the actionfilterattribute?
Currently I have an actionfilterattribute similar to this:
public class PermissionRequired : ActionFilterAttribute{
private readonly Role reqrole;
public PermissionRequired(Role reqRole)
{
reqrole = reqRole;
}
public override void OnActionExecuting(ActionExecutingContext filterContext) {
var ctrl = (GeneralController)filterContext.Controller;
if (!ctrl.CurrentUser.InRole(reqrole)) {
//some code to redirect this to a certain page
}
base.OnActionExecuting(filterContext);
}
}
and on the GeneralController to get the current User
public class GeneralController : Controller
private User currentUser;
public User CurrentUser {
get {
if (currentUser != null)
return currentUser;
int currentUserId = Convert.ToInt32(httpContext.User.identity.Name);
if (currentUserId != 0) {
this.currentUser = Tds.Users.FirstOrDefault(u => u.Id == currentUserId)
}
return currentUser;
}
}
and on the controllers that will inherit this attribute
[PermissionRequired(Role.Moderator)]
public class SomeControllerThatNeedsPermission
{
[PermissionRequired(Role.SuperAdmin)]
public ActionResult SomeActionThatNeedsPermission()
{
}
}
so, anybody help is appreciated.. even comments or thoughts are welcome :D
Thanks much!
It seems like you are not using custom membership here. In which case doing this with a actionfilterattribute is somewhat pointless, but nonetheless do able.
This is an excellent article on the same subject - extending the AuthorizeAttribute to perform role based validation and return custom errors...
The value in doing that also only comes across (as explained in the article) when you wish to show users whats going on when the Authorization fails (the 401 is not shown it turns into a 302 internally in the mvc plumbing)
I want my login page to be SSL only:
[RequireHttps]
public ActionResult Login()
{
if (Helper.LoggedIn)
{
Response.Redirect("/account/stats");
}
return View();
}
But obviously it doesn't work on localhost when I develop and debug my application. I don't wanna use IIS 7 with SSL certificates, how can I automatically disable the RequireHttps attribute?
Update
Based on info provided by StackOverflow users and ASP.NET MVC 2 source code I created the following class that solves the problem.
public class RequireSSLAttribute : FilterAttribute, IAuthorizationFilter
{
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (!filterContext.HttpContext.Request.IsSecureConnection)
{
HandleNonHttpsRequest(filterContext);
}
}
protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.Url.Host.Contains("localhost")) return;
if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The requested resource can only be accessed via SSL");
}
string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
And it's used like this:
[RequireSSL]
public ActionResult Login()
{
if (Helper.LoggedIn)
{
Response.Redirect("/account/stats");
}
return View();
}
The easiest thing would be to derive a new attribute from RequireHttps and override HandleNonHttpsRequest
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.Request.Url.Host.Contains("localhost"))
{
base.HandleNonHttpsRequest(filterContext);
}
}
HandleNonHttpsRequest is the method that throws the exception, here all we're doing is not calling it if the host is localhost (and as Jeff says in his comment you could extend this to test environments or in fact any other exceptions you want).
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
if (!HttpContext.Current.IsDebuggingEnabled) {
filters.Add(new RequireHttpsAttribute());
}
}
#if (!DEBUG)
[RequireHttps]
#endif
public ActionResult Login()
{
if (Helper.LoggedIn)
{
Response.Redirect("/account/stats");
}
return View();
}
You can encapsulate this requirement in a derived attribute:
class RequireHttpsNonDebugAttribute : RequireHttpsAttribute {
public override void HandleNonHttpsRequest(AuthorizationContext ctx) {
#if (!DEBUG)
base.HandleNonHttpsRequest(ctx);
#endif
}
}
MVC 6 (ASP.NET Core 1.0):
The proper solution would be to use env.IsProduction() or env.IsDevelopment().
Example:
Startup.cs - AddMvc with a custom filter:
public void ConfigureServices(IServiceCollection services)
{
// TODO: Register other services
services.AddMvc(options =>
{
options.Filters.Add(typeof(RequireHttpsInProductionAttribute));
});
}
Custom filter inherit from RequireHttpsAttribute
public class RequireHttpsInProductionAttribute : RequireHttpsAttribute
{
private bool IsProduction { get; }
public RequireHttpsInProductionAttribute(IHostingEnvironment environment)
{
if (environment == null)
throw new ArgumentNullException(nameof(environment));
this.IsProduction = environment.IsProduction();
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (this.IsProduction)
base.OnAuthorization(filterContext);
}
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
if(this.IsProduction)
base.HandleNonHttpsRequest(filterContext);
}
}
Design decisions explained:
Use environment IsProduction() or IsDevelopment() over e.g.
"#if DEBUG". Sometimes we release/publish in DEBUG mode on our test server and don't want to disable this security requirement. This needs to be disabled only in localhost/development (since we are too lazy to setup localhost SSL in IIS Express or whatever we use locally).
Use filter in Startup.cs for global setup (since we want this to apply everywhere). Startup should be responsible for registering and setting up all global rules. If your company employ a new developer, she would expect to find global setup in Startup.cs.
Use RequireHttpsAttribute logic since it's proven (by Microsoft). The only thing we want to change is when logic is applied (production) and when it's not (development/localhost). Never use "magical" strings like "http://" and "https://" when it can be avoided by reusing a Microsoft component created to provide the same logic.
Above I would consider the "proper" solution.
Note:
As an alternative, we could make a "class BaseController : Controller" and make all our controllers inherit from "BaseController" (instead of Controller). Then we only have to set the attribute 1 global place (and don't need to register filter in Startup.cs).
Some people prefer the attribute style. Please note this will eliminate design decision #2's benefits.
Example of usage:
[RequireHttpsInProductionAttribute]
public class BaseController : Controller
{
// Maybe you have other shared controller logic..
}
public class HomeController : BaseController
{
// Add endpoints (GET / POST) for Home controller
}
If I want only administrator to access the action called "ManagerUser", I know I can do this:
[Authorize( Roles = Constants.ROLES_ADMINISTRATOR )]
public ActionResult ManageUser( string id )
{
}
What if I want to give everyone access except to administrator? I do not want to write all roles up there on function :|.
Any recommendations/way outs?
You can create your own custom Authorize attribute, something like "AuthorizeAllExceptAdmin." Within that class you would simply need to check whether or not the current user was an admin, and if they were reject it, otherwise accept it.
Here's a good tutorial, but you'll probably end up with something like:
public class AuthorizeAllExceptAdmin : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return !httpContext.User.IsInRole(Constants.ROLES_ADMINISTRATOR);
}
}
Then your controller method becomes:
[AuthorizeAllExceptAdmin]
public ActionResult SomethingOnlyNonAdminsCanDo()
{
}
Here's an example of the custom attribute that takes in roles to deny.
public class DoNotAuthorize : AuthorizeAttribute
{
private IEnumerable<string> _rolesToReject;
public DoNotAuthorize(IEnumerable<string> rolesToReject)
{
_rolesToReject = rolesToReject;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
foreach (var role in _rolesToReject)
{
if (httpContext.User.IsInRole(role))
return false;
}
return true;
}
}
Then your controller method becomes:
[DoNotAuthorize(new [] {Constants.ROLES_ADMINISTRATOR})]
public ActionResult SomethingOnlyNonAdminsCanDo()
{
}
I would put some thought into it before choosing one of the above options. If you think you'll have several methods (or entire controllers) with similar authorization requirements (i.e, several actions an admin can not perform) then I would stick with the non-parameterized custom attribute. This way, you can evolve them all together (by only changing the custom attribute) later on. For example, maybe later on you want admins to be able to go into a special mode where they can perform these actions.
Alternatively, if the autorization is more varied amongst the actions, then using the parameterized list makes sense, since they'll evolve relatively independently.
Besides creating a custom AuthorizeAttribute, suggested by manu, you could use PrincipalPermission, with a Deny-SecurityAction:
[PrincipalPermission(SecurityAction.Deny, Role="Administrator")]
In my app I don't use roles so I have to query the database to determine whether the user has access or not. The benefits of the code below is that you can redirect the user to a certain action very easily. I explained the code in my blog post at http://blog.athe.la/2009/12/implementing-permission-via-windows-authentication-in-asp-mvc-using-action-filters/
public class DatabaseRepository()
{
private readonly DatabaseDataContext db = new DatabaseDataContext();
public bool UserHasPermission(string userLogon) {
return (from permission this.db.Permissions
where permission.HasPermissionSw == true
select permission).Contains(userLogon);
}
}
public class UserHasPermission: ActionFilterAttribute
{
private readonly DatabaseRepository databaseRepository = new DatabaseRepository();
private readonly string redirectAction;
public UserHasPermission(string redirectTo)
{
this.redirectAction = redirectTo;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string userLogon = filterContext.HttpContext.User.Identity.Name;
if (!this.databaseRepository.UserHasPermission(userLogon))
{
string routeController = filterContext.Controller.ControllerContext.RouteData.Values["controller"];
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = routeController, action = this.redirectAction }));
}
}
}
Your controller would then look something like this:
[UserHasPermission("NoAccess")]
public ActionResult SecretArea()
{
// run all the logic
return View();
}
public ActionResult NoAccess()
{
return View();
}