We're busy porting a legacy ASP.NET web forms application to MVC. Some modules are finished with their valid Authorize attributes correctly set up, but only 1 module is going live.
So we must prevent the user from navigating to different modules (which are there, but not "live" yet). We don't want to meddle with the existing Authorize attributes, but users are currently not allowing access to these modules.
Here are my thoughts and shortfalls:
In Global.asax subscribe to Application_AuthenticateRequest and have a list of "Live" controllers, check the Request URL and throw and redirect to "Not Authorized page" if necessary. But how then I would would have to manually take routing into account where the URL may mysite/ could route to mysite/Foo/Bar/.
Could the traditional web.config authorization be used for this scenario? (This would be easier to maintain than number 1, but the web is littered with Don't do this in MVC's)
Something like this, where Customer is the controller:
<location path="Customer">
<system.web>
<authorization>
<deny users="*" />
</authorization>
</system.web>
</location>
Alternatively take the plunge, comment out ALL the Authorize attributes from the controllers which aren't live :( hoping not to go down this route...
Any push in a better direction would be greatly appreciated.
You could use the asp.net mvc filter on this case.
public class YourCustomFilter : IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
List<Filter> result = new List<Filter>();
var routeData = controllerContext.HttpContext.Request.RequestContext.RouteData;
var controller = routeData.GetRequiredString("controller");
var action = routeData.GetRequiredString("action");
if (controller != "livecontrollername" && action != "liveactionname")
{
result.Add(new Filter(new YourCustomAuthorizeAttribute(), FilterScope.Global, null));
}
return result;
}
}
public class YourCustomAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
//Do something to prevent user from accessing the controller here
}
Then register this custom filter in Global.ascx, App_Start:
protected void Application_Start()
{
FilterProviders.Providers.Add(new YourCustomFilter());
}
Related
I am building an intranet site where users will be on the corporate domain and have different permission levels. I'm currently using <authentication mode="Windows"/> to control site access, but it seems like I should be using ASP.NET Identity.
For example, say my application is a dashboard for each department in the organization. I want to create a single AD group called DashboardUsers and put everyone that can touch the site in this group.
I also want to restrict the views in the dashboard. For example, I only want the IT group to see their view, and the Finance folks see theirs, etc.
Question -- should I be using Windows Authentication to control access to the site, and then use ASP.NET Identity for user level permissions?
I have done something similar to this using only WindowsAuthentication. You can tag your actions with the Authorize Attribute:
[Authorize(Roles = #"DashboardUsers")]
As long is this user is a member of the DashboardUsers AD group they will get access to this action. It seems like MVC magic, but it really is that simple.
Unfortunately this approach will not allow you to overload an action for different Roles as the Authorize Attribute is not part of the method's signature. In your views, you would have to show different anchor tags based on the current users role.
ie:
[Authorize(Roles = #"DashboardUsers\Manager")]
public ActionResult IndexManagers()
{
..
}
or
[Authorize(Roles = #"DashboardUsers\Finance")]
public ActionResult IndexFinance()
{
..
}
EDIT AFTER COMMENT:
Since your Identity is coming from AD, you could use logic in your controller like:
if(User.IsInRole("Finance"))
{
..
}
else if(User.IsInRole("IT"))
{
..
}
And this will check which AD Group they belong too. I know it's not very elegant, but I can't imagine mixing Windows Auth with a custom identity and managing permissions in your own db would be elegant either.
I have run into this dilemma before and wound up creating a custom role provider that i used in conjunction with windows authentication. I'm not sure you need the OWIN middleware when authenticating against AD.
public class MyAwesomeRoleProvider : RoleProvider
{
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
// i talk to my database via entityframework in here to add a user to a role.
}
// override all the methods for your own role provider
}
config file
<system.web>
<authentication mode="Windows" />
<roleManager enabled="true" defaultProvider="MyAwesomeRoleManager">
<providers>
<clear />
<add name="MyAwesomeRoleManager" type="MyAwesomeNamespace.MyAwesomeRoleProvider" connectionStringName="MyAwesomeContext" applicationName="MyAwesomeApplication" />
</providers>
</roleManager>
</system.web>
Normally use authorization settings to interact with the roles.
<location path="Register.aspx">
<system.web>
<authorization>
<allow roles="Administrator"/>
<deny users="?"/>
</authorization>
</system.web>
</location>
But I would like to have this setup in the database in a table
tblAuthorization
-IdAuthorization (Identy(1,1))
-IdCompany=5
-IdRol=5 (Administrator”)
-Path=”Register.aspx”
Is there a setting for a class to do this? There is something like Profile, RoleProvider ..
<roleManager enabled="true" defaultProvider="MyAccessRolProvider">
<providers>
<clear />
<add name="MyAccessRolProvider" type="MyAccessRolProvider" connectionStringName="MyConnectionString" applicationName="/" />
<add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
</providers>
</roleManager>
Right now the only I think that can to implement a validation in the Page_Load event
And if it is invalid making a Redirect but I would do a little more "Professional"
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
if(!ValidateAuthorization())
{
Response.Redirect("Login.aspx");
}
}
}
Could help me with examples?
Thank you very much in advance
Your best bet is to implement a custom RoleProvider - you seem to be heading in this direction as your question includes a configuration section with a custom RoleProvider (MyAccessRolProvider).
You need to specify the fully-qualified name of your RoleProvider, e.g. MyNamespace.MyAccessRoleProvider, MyAssembly.
MSDN describes how to do this, including a sample implementation.
With the understanding that you are taking about authorization at the page level you could add a HTTP module (IHttpModule) and use your module to do the authorization checking. If the authorization failed then you could redirect as appropriate.
Something like this might work:
public class MyAuthorizationModule : IHttpModule
{
public void Init(HttpApplication application)
{
application.AuthorizeRequest += (new EventHandler(AuthorizeRequest));
}
private void AuthorizeRequest(Object source, EventArgs e)
{
bool isAuthorized = //your code here to check page authorization
if (!isAuthorized)
{
var response = //will need to get current response
response.Redirect("Login.aspx", true);
}
}
}
Note: Don't forget you'll need to add the module to your web application.
There is no out of the box support for this as far as I know however I would suggest you implement something as follows:
Create a new class that derives from AuthorizeAttribute, add an ID property and override the "AuthorizeCore" method.
public class CustomAutorizeAttribute : AuthorizeAttribute
{
public Guid ActionId { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//Check user has access to action with ID = ActionId
//implementation omitted
}
}
You can add a new database table to hold on the Actions and their associated ID values. You can either add the record to the table manually or write some code to do this using reflection (I would advise this if you have quite a few actions and are likely to add more in the future).
Once this is done you can assign users or application roles access to these actions.
In you custom Authorize attribute you have to check whether the user has access to the given action returning true or false accordingly from the AuthorizeCore method.
I would advise grabbing a collection of all actions a user has access to when a user logs in and storing them in either the server cache or some kind of distributed cache so that you aren't hitting your database on every request.
UPDATE - Sorry just realised you're using ASP.NET Forms and not MVC. My approach is for an MVC application so its probably not relevant although I'll leave it up here in case any MVC developers happen to stumble across your question.
I have enabled form authentication in my ASP.NET MVC web application. I want to allow anonymous users access only to some specific pages, including Register.cshtml for instance. I was able to allow access to my CSS-file from my root web.config by doing this.
<location path="Content/Site.css">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
Now I want to allow anonymous access to other pages, like Home and Register. Do any body know how to achieve this?
In MVC you normally use the [Authorize] attribute to manage authorization. Controllers or individual actions that are dressed with that attribute will require that the user is authorized in order to access them - all other actions will be available to anonymous users.
In other words, a black-list approach, where actions that require authorization are black-listed for anonymous users using [Authorize] - all actions (not dressed with the attribute) will be available.
Update:
With MVC4 a new attribute has been introduced, namely the [AllowAnonymous] attribute. Together with the [Authorize] attribute, you can now take a white-list approach instead. The white-list approach is accomplished by dressing the entire controller with the [Authorize] attribute, to force authorization for all actions within that controller. You can then dress specific actions, that shouldn't require authorization, with the [AllowAnonymous] attribute, and thereby white-listing only those actions. With this approach, you can be confident that you don't, by accident, forget to dress an action with the [Authorize], leaving it available to anyone, even though it shouldn't.
Your code could then be something like this:
[Authorize]
public class UserController : Controller {
[AllowAnonymous]
public ActionResult LogIn () {
// This action can be accessed by unauthorized users
}
public ActionResult UserDetails () {
// This action can NOT be accessed by unauthorized users
}
}
In the Web.config i had the below authorization
<authorization>
<deny users ="?"/>
</authorization>
this causes the
[AllowAnonymous]
not work correctly, i had to remove that authorization of my Web.config, and in all the controllers put the line
[Authorize]
before the declaration of the class, to work correctly.
I'm trying to setup Forms Authentication in an asp.net mvc 2 application that will be hosted on IIS 6. There's an issue somewhere in my routing, but I can't pinpoint exactly where it is.
Here is the route entries I'm using to route the mvc requests through the aspx processing on IIS 6. These may or may not be the "right" way, but they do work on the server at current.
routes.MapRoute(
"Default",
"{controller}.aspx/{action}/{id}",
new { action = "LogOn", id = "" }
);
routes.MapRoute(
"Root",
"",
new { controller = "Main", action = "LogOn", id = "" }
);
I've put the [Authorize] attribute on my Main controller.
In my web.config I have:
<authentication mode="Forms">
<forms loginUrl="~/Main.aspx/LogOn" timeout="2880"/>
</authentication>
When the application starts, a blank page loads. The page is quite literally blank. I haven't found a way to amend the loginUrl to actually execute the LogOn action & View for my Main controller.
Edited
Just as an fyi, I've setup my routing based on this article so that the mvc routing can work on IIS 6.
http://www.asp.net/mvc/tutorials/using-asp-net-mvc-with-different-versions-of-iis-cs
I'm guessing the problem here is that the windows form authentication settings aren't syncing with the routes setup so the app can run on IIS 6 via the aspx extension.
Anyone have thoughts on how I could fix this?
Edit 2
Tried adding the following route:
routes.MapRoute(
"Login",
"Login",
new { controller = "Main", action = "LogOn" }
);
Amended the web.config to:
<authentication mode="Forms">
<forms loginUrl="~/Login" timeout="2880"/>
</authentication>
The result is the same white screen as I originally got. It seems like the page doesn't get processed at all. Viewing the source from page generated shows absolutely nothing....no markup...no html declaration....just nothing.
EDIT 3
It seems that I can't seem to get the correct routing configured with the default forms authentication via the web.config. To circumvent this, I've created my own Authorize attribute class. At current, I only care that the user has logged into the system. To accomodate this, I moved the LogOn & LogOff actions to an Account controller. I've remapped the root path to point to this controller. In my custom Authorize attribute, I check to see if the user is logged in and redirect them back to the LogOn page if they aren't. Here is the code:
routes.MapRoute(
"Root",
"",
new { controller = "Account", action = "LogOn", id = "" }
);
And here's the code for the RequireLoginAttribute class I derrived.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireLoginAttribute : AuthorizeAttribute, IAuthorizationFilter
{
#region IAuthorizationFilter Members
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.Request.IsAuthenticated)
{
//This didn't work...it would try routing to urls like
//http://localhost:1524/Main.aspx/Account.aspx/Logon
//new RedirectResult("Account.aspx/Logon");
//This seems sloppy to me somehow, but it works.
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Account", action = "LogOn" }));
}
}
#endregion
}
Now, I can just apply the [RequireLogin] attribute to the Main controller and it ensures the user must be authenticated.
For the curious, and completeness of this scenerio, I am using the following code in the LogOn action (repository isn't ready yet so things are hard coded):
public ActionResult LogOn(LogOnModel login, String returnUrl)
{
if (ModelState.IsValid)
{
FormsAuthentication.SetAuthCookie(login.UserName, false);
return Redirect(returnUrl ?? Url.Action("NextPage", "Main"));
}
else
{
return View(login);
}
}
The returnUrl is a throwback to the Windows Forms authentication. Since I can't seem to get that working here, the parameter will always be null.
Please, critique this if you see specific areas that need improvement. I'm reading what I can and trying to do things right, so all input is greatly appreciated. Thanks!
If you need the .aspx for the Default Root then why don't you need it for the login route ?
You could do a couple of things then
Actually create a ASP.NET page called Login.aspx and put it in the root of the folder (Authentication will work for your mvc pages as well)
Change your login route to say
routes.MapRoute("Login","Login.aspx",new { controller = "Main", action = "LogOn" });
You should also take a look at to see what route your actually hitting at any time.
http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx
Remember that the order you write your routes in Global matters. It stops checking when it finds one that works so your catch all should be last.
The details I posted in EDIT 3 summarize the solution to this issue. I appreciate all of your input into this question, but I've resolved it. I would have liked to have gotten the "out of the box" forms authentication working, but this solution serves well enough. If we move to IIS 7, I think all of this will become moot anyhow.
Thanks again for your help guys.
I am using the Authorize attribute like this:
[Authorize (Roles="Admin, User")]
Public ActionResult Index(int id)
{
// blah
}
When a user is not in the specified roles, I get an error page (resource not found). So I put the HandleError attribute in also.
[Authorize (Roles="Admin, User"), HandleError]
Public ActionResult Index(int id)
{
// blah
}
Now it goes to the Login page, if the user is not in the specified roles.
How do I get it to go to an Unauthorized page instead of the login page, when a user does not meet one of the required roles? And if a different error occurs, how do I distinguish that error from an Unauthorized error and handle it differently?
Add something like this to your web.config:
<customErrors mode="On" defaultRedirect="~/Login">
<error statusCode="401" redirect="~/Unauthorized" />
<error statusCode="404" redirect="~/PageNotFound" />
</customErrors>
You should obviously create the /PageNotFound and /Unauthorized routes, actions and views.
EDIT: I'm sorry, I apparently didn't understand the problem thoroughly.
The problem is that when the AuthorizeAttribute filter is executed, it decides that the user does not fit the requirements (he/she may be logged in, but is not in a correct role). It therefore sets the response status code to 401. This is intercepted by the FormsAuthentication module which will then perform the redirect.
I see two alternatives:
Disable the defaultRedirect.
Create your own IAuthorizationFilter. Derive from AuthorizeAttribute and override HandleUnauthorizedRequest. In this method, if the user is authenticated do a redirect to /Unauthorized
I don't like either: the defaultRedirect functionality is nice and not something you want to implement yourself. The second approach results in the user being served a visually correct "You are not authorized"-page, but the HTTP status codes will not be the desired 401.
I don't know enough about HttpModules to say whether this can be circumvented with a a tolerable hack.
EDIT 2:
How about implementing your own IAuthorizationFilter in the following way: download the MVC2 code from CodePlex and "borrow" the code for AuthorizeAttribute. Change the OnAuthorization method to look like
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (AuthorizeCore(filterContext.HttpContext))
{
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
// Is user logged in?
else if(filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// Redirect to custom Unauthorized page
filterContext.Result = new RedirectResult(unauthorizedUrl);
}
else {
// Handle in the usual way
HandleUnauthorizedRequest(filterContext);
}
}
where unauthorizedUrl is either a property on the filter or read from Web.config.
You could also inherit from AuthorizeAttribute and override OnAuthorization, but you would end up writing a couple of private methods which are already in AuthorizeAttribute.
You could do this in two ways:
Specify the error the HandleError-attribute, and give a view that should be shown:
[HandleError(ExceptionType = typeof(UnAuthorizedException),
View = "UnauthorizedError")]
You can specify several different ExceptionTypes and views
Create a custom ActionFilter, check there for credentials, and redirect to a controller if the user is unauthorized.: http://web.archive.org/web/20090322055514/http://msdn.microsoft.com/en-us/library/dd381609.aspx
Perhaps a 403 status code is more appropriate based on your question (the user is identified, but their account is not privileged enough). 401 is for the case where you do not know what priveleges the user has.
And HttpUnauthorizedResult (this comes as reuslt of the AuthorizeAtrribute) just sets StatusCode to 401. So probably you can setup 401 page in IIS or custom error pages in web.config. Of course, you also have to ensure that access to your custom error page is not requires authorization.
Just override the HandleUnauthorizedRequest method of AuthorizeAttribute. If this method is called, but the user IS authenticated, then you can redirect to your "not authorized" page.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { Area = "", Controller = "Error", Action = "Unauthorized" }));
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}