MVC 6 Areas and multiple login pages redirect - asp.net

I'd been searching for a solution to this problem for quite a long time but unfortunately haven't found any nice and elegant way to handle it.
Here are the details:
My MVC 6 application use Areas. Each area has separate directories for the Controllers, Views etc.
Authentication is based on the standard out of the box web application template with user accounts stored in sql server
What I want to achieve is:
When user enters /AreaA/Restricted/Page then he is redirected into /AreaA/Account/Login
When user enters /AreaB/Restricted/Page then he is redirected into /AreaB/Account/Login etc...
Even though I can change the stanard login page redirect from "/Account/Login" into something different like this:
services.Configure<IdentityOptions>(options=> {
options.Cookies.ApplicationCookie.LoginPath =
new Microsoft.AspNet.Http.PathString("/HardcodedAreaName/Account/Login");
});
I am not able to redirect into different actions/login pages for each area.
Prior to MVC 6 I was able to use AuthorizeAttribute with url parameter:
public class CustomAuthorization : AuthorizeAttribute
{
public string Url { get; set; }
// redirect to login page with the original url as parameter.
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectResult(Url + "?returnUrl=" + filterContext.HttpContext.Request.Url.PathAndQuery);
}
}
and then passing the area dependent url by decorating each controller:
[CustomAuthorization(Url = "/Admin/Account/Login"]
public class AdminAreaController : Controller
{ ...
But it does not work anymore :(

Try the following and see if it works (I did try this and it works fine, but not sure If I have covered all scenarios):
The place where you register you CookieAuthentication middleware, you can do something like
app.UseCookieAuthentication(o =>
{
o.LoginPath = "/area1/login1";
o.AuthenticationScheme = "scheme1";
//TODO: set other interesting properties if you want to
});
app.UseCookieAuthentication(o =>
{
o.LoginPath = "/area2/login2";
o.AuthenticationScheme = "scheme2";
//TODO: set other interesting properties if you want to
});
On you controller/action, specify the authentication scheme..example:
[Authorize(ActiveAuthenticationSchemes = "scheme1")]
public IActionResult Test1()
{
return Content("Test1");
}
[Authorize(ActiveAuthenticationSchemes = "scheme2")]
public IActionResult Test2()
{
return Content("Test2");
}

Related

Adding extra step to ASP.NET MVC authentication

I have an MVC 5 website running using standard forms authentication.
However I need to add an extra step to the user's login process. Once the user has been authenticated we look up whether or not they have access to multiple offices. If they do we need to show them a list of offices and they must choose one.
This is a mandatory step and they cannot be considered logged on until they do it.
Do we need to create our own authentication or should I add a check to a BaseController?
You can extend the implementation of the built-in authentication:
public class OfficeSelectionAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var result = base.AuthorizeCore(httpContext);
if (result)
{
if (IsOfficeSelected())
{
return true;
}
httpContext.Response.RedirectToRoute("OfficeSelection Route");
httpContext.Response.Flush();
}
return false;
}
private bool IsOfficeSelected()
{
//office selection check
}
}
Then you need to use this filter instead of the default one:
[OfficeSelectionAuthorize]
public class AccountController : Controller
{
//action methods
}

Using object Helper Methods to implement authorization rules

I have the following:-
I am working on an asset management system using Asp.net MVC4 with windows authentication enabled.
The system allow to specify what actions a group of users can do(for example certain group can have the authority to add new physical asset , while they can only read certain logical asset, and so on).
So I found that using the build-in Asp.net role management, will not allow me to have the level of flexibility I want. So I decided to do the following:-
I have created a table named “group” representing the user groups. Where users are stored in active directory.
I have created a table named ”Security Role” which indicate what are the permission levels each group have on each asset type(edit, add, delete or view)per asset type.
Then on each action methods , I will use Helper methods to implement and check if certain users are within the related group that have the required permission ,, something such as
On the Car model object I will create a new helper method
Public bool HaveReadPermison(string userName) {
//check if this user is within a group than have Read permission on CARS, //OR is within a GROUP THAT HAVE HIGHER PERMISON SUCH AS EDIT OR ADD OR //DELETE.
}
Next, On the Action method, I will check if the user has the Read permission or not by calling the action method:-
public ActionResult ViewDetails(int id) { // to view transportation asset type
Car car = repository.GetCar(id);
if (!car.HaveReadPermision(User.Identity.Name)) {
if (car == null)
return View("NotFound");
else
return View(car);
}
else
return view (“Not Authorized”);
So can anyone advice if my approach will be valid or it will cause problem I am unaware about.
Regards
In my opinion, once you have decided to use the ASP membership and role providers you can keep leveraging them also for authorization, simply using the Authorize attribute. This will also allow to restrict access by user names and roles.
What the attribute won't do is Action-based authorization. In that case there are a few options but in my opinion this could be brilliantly resolved by a Custom Action Filter based loosely on the following code:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CheckUserPermissionsAttribute : ActionFilterAttribute
{
public string Model { get; set; }
public string Action { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var user = filterContext.HttpContext.User.Identity.Name; // or get from DB
if (!Can(user, Action, Model)) // implement this method based on your tables and logic
{
filterContext.Result = new HttpUnauthorizedResult("You cannot access this page");
}
base.OnActionExecuting(filterContext);
}
}
Yes, it is vaguely inspired to CanCan, which is a nice Ruby gem for this kind of things.
Returning Unauthorized (401) will also instruct your server to redirect to the login page if one is specified. You may want to work on that logic if you want to redirect somewhere else. In that case you should do:
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary { { "Controller", "Home" }, { "Action", "Index" } });
and choose the appropriate controller/action pair.
You can use the attribute like this:
[CheckUserPermissions(Action = "edit", Model = "car")]
public ActionResult Edit(int id = 0)
{
//..
}
Let me know if that works nicely for you.
The approach you took looks reasonable, but I would add few changes:
What if you forgot to call HaveReadPermision method? And checking authotization from Actions is not the cleanest solution either, that is not an Action reponsibility.
It is better to keep authorization logic separately. For instance you can create a decorator over you repository which will check the permissions of the current User:
public class AuthorizationDecorator: IRepository
{
public AuthorizationDecorator(IRepository realRepository, IUserProvider userProvider)
{
this.realRepository = realRepository;
this.userProvider = userProvider;
}
public Car GetCar(int id)
{
if(this.UserHaveReadPermission(this.userProvider.GetUserName(), Id))
{
return this.realRepository.GetCar(id);
}
else
{
throw new UserIsNotAuthorizedException();
}
}
private bool UserHaveReadPermission(string username, int id)
{
//do your authorization logic here
}
}
IUserProvider will return curent user name from httpRequest.
After doing the change you don't need to warry about authorization when writing Actions

How to share a common object in each page request of a ASP.net MVC 4 webapp?

I come from "regular" asp.net so i'm a bit (totally) lost with MVC.
What I was doing with my own asp.net programmation pattern :
I have one custom class objet which represent the "page" and its properties (like mypage.loadJquery, mypage.isLogged, mypage.Title, custom cache logic, etc.)
This class is instanciate once on top of each ASHX page, I then manipulate a stringbuilder to produce HTML and spit it right at the browser at the end.
Having only one request on the ASHX page, i can use my page object instanciated at the top till the end when calling final response.write
Now i'm trying to go for MVC. I "kind of" understood the M/V/C model and the routing concept. I would like to keep my custom "page" object but I lost my page life cycle and I definitely don't know how to instanciate my page object ONCE in at the top of every call.
I need this instanciated ONCE shared object across every models, controllers, views, partial views, htmlhelper...
I realize MVC pattern might be confusing for me at this moment, bu how could I try to reproduce my need ?
(Very concrete exemple : On every request i need to check if the user is logged via his cookies. If it is I round trip the database to get user infos. Then I DO NEED THESE INFOS ON PRATICALLY EVERY model / controller / view of the app, but of course don't want to round back each time to security check and database querying, how can i have these info on the whole mvc cyle ?)
In my project I create interface IViewModel that contains all fields that I need in my layout/masterpage and set is as model of it so I can easily use them:
IViewModel.cs
public interface IViewModel
{
string Title { get; set; }
User User { get; set; }
}
Layout.cshtml
#model IViewModel
<html>
<head>
<title>#Model.Title</title>
</head>
<body>
#if (Model.User.IsAuthenticated) {
You are logged as #Model.User.Name
}
</body>
</html>
All my models implement that interface (or inherit from ViewModelBase that is default implementation of that class). Additionally I have custom action filter that check if returned ActionResult is (Partial)ViewResult and if Model of it implement my IViewModel interface and fill data in that interface.
public FillViewModelAttribute : ActionFilterAttribute
{
public override OnActionExecuted(ActionExecutedContext context)
{
var viewResult = context.Result as ViewResult;
if (viewResult != null && viewResult.Model is IViewModel)
{
var viewModel = (IViewModel)viewResult.Model;
// fill data
}
}
}
I created many projects like this. Basically, you can create a base controller class where all the other controllers inherit from it.
[Authorize]
public class BaseController : Controller
{
private Instructor _G_USER = null;
protected Instructor G_USER
{
get
{
if (_G_USER == null)
{
_G_USER = Your DB query here
ViewData["G_USER"] = _G_USER;
}
return _G_USER;
}
}
}
Then in your every child class, you can do
[Authorize]
public class YourController : BaseController
{
public ActionResult Index()
{
if(!G_USER.CAN_DO_THIS) throw new NoPermissionException();
return View();
}
}
To use the User in the view, create an extension method.
public static class ExtentionMethods
{
public static USER G_USER(this ViewPage page)
{
return (USER)page.ViewData["G_USER"];
}
}
Then use in the page like this
<%=this.G_USER().....%>

Add token parameter to all urls inside an asp.net mvc 2 site

I've integrated some pages written in ASP.NET MVC 2, into a classic webform app.
Everything works well except the authentication system.
The authentication system is using some token added to the url like :
/Account/Profil/Details.aspx?AUTHID=2ddc098a-cf0b-fd81-afb7-d41f35010b9f
When i reach my asp.net mvc pages (all these pages must be secured), they must include that AUTHID parameter.
I'm using the core Webform control to secure the pages, and this control check for the AUTHID token in the url. So basicly my route must include the
?AUTHID=2ddc098a-cf0b-fd81-afb7-d41f35010b9f
What the best and clever way to do this ?
I don't want to pass the AUTHID parameter manually in all controller actions.
Thanks for your help.
You can solve your problem by extending the ASP.NET routing mechanism. Just create a custom route and override the GetVirtualPath function.
public class TokenizedRoute : Route
{
public TokenizedRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler)
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
string tokenValue = "your token value";
values.Add("AUTHID", tokenValue);
return base.GetVirtualPath(requestContext, values);
}
}
See my blog post for more details.
You could use a jQuery solution to append a token to the query string of all links:
$("a").each(function (index, link)
{
$(link).attr("href", $(link).attr("href") + "?AUTHID=" + token);
});
But I agree with dknaack, I would say you should reconsider your authentication logic if at all possible.
You can save the AuthId in the Session object and create a custom Authorize Attribute.
Attribute
public class CustomAuthorize : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
// your custom logic depending on Session["AuthId"]
return httpContext.Session["AuthId"] != null;
}
}
Controller
public class MyController : Controller
{
[CustomAuthorize]
public ActionResult MyActionMethod()
{
return View();
}
}
hope this helps

ASP.Net MVC 3 Strange Session Behaviour

I have an mvc 3 app for which I'm implementing authorization using my own login view which checks if the users name and password are allowed and then sets a variable in the session to say that the user is loggged in. This kind of works but for one particular view it is behaving in a strange undesirable way. The said view contains a form which I use to input some data and upload a file. For some reason which I can't figure out, after this form is posted a new session is started and therefore the variable which remembered that the user was logged in is reset to false and subsequently the login page is displayed again.
I'm lost as to why the application is starting a new session at this point? I have not instructed it to do this. Can anyone recommend solutions to stop this behaviour and get it to keep the old session?
Thanks.
UPDATE - Some Code:
Note the session seems to be terminated immediately after the response to the posted Create form
CMS controller which uses a custom Autorize attribute called "RDAutorize" on all actions:
[RDAuthorize]
public class PhotoCMSController : Controller
{
public ActionResult Create()
{
/* Code omitted: set up a newPhoto object with default state */
/* Display view containing form to upload photo and set title etc. */
return View("../Views/PhotoCMS/Create", newPhoto);
}
[HttpPost]
public ContentResult Upload(int pPhotoId)
{
/* Code ommited: receive and store image file which was posted
via an iframe on the Create view */
string thumbnail = "<img src='/path/to/thumb.jpg' />";
return Content(thumbnail);
}
[HttpPost]
public ActionResult Create(string pPhotoTitle, string pCaption etc...)
{
/*Code omitted: receive the rest of the photo data and save
it along with a reference to the image file which was uploaded
previously via the Upload action above.*/
/* Display view showing list of all photo records created */
return View("../Views/PhotoCMS/Index", qAllPhotos.ToList<Photo>());
/* **Note: after this view is returned the Session_End() method fires in
the Global.asax.cs file i.e. this seems to be where the session is
being lost** */
}
}/*End of CMS Controller*/
Custom Authorize action filter:
public class RDAuthorize : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Boolean authorized = Convert.ToBoolean(
HttpContext.Current.Session["UserIsAuthorized"]
);
if (!authorized) {
/* Not logged in so send user to the login page */
filterContext.HttpContext.Response.Redirect("/Login/Login");
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {}
public override void OnResultExecuting(ResultExecutingContext filterContext) {}
public override void OnResultExecuted(ResultExecutedContext filterContext) {}
}/*End of Authorize Action Filter*/
Login controller:
public class LoginController : Controller
{
private PhotoDBContext _db = new PhotoDBContext();
public ActionResult Login()
{
string viewName = "";
Boolean authorized = Convert.ToBoolean(Session["UserIsAuthorized"]);
if (authorized)
{
viewName = "../Views/Index";
}
else
{
viewName = "../Views/Login/Login";
}
return View(viewName);
}
[HttpPost]
public ActionResult Login(string pUsername, string pPassword)
{
string viewName = "";
List<Photo> model = new List<Photo>();
var qUsers = from u in _db.Users
select u;
foreach (User user in qUsers.ToList<User>())
{
/* If authorized goto CMS pages */
if (pUsername == user.Username && pPassword == user.Password)
{
Session["UserIsAuthorized"] = true;
var qPhotos = from p in _db.Photos
where p.IsNew == false
select p;
model = qPhotos.ToList<Photo>();
viewName = "../Views/PhotoCMS/Index";
break;
}
}
return View(viewName, model);
}
}/* End of Login controller */
Turns out the whole ASP.Net application was restarting because as part of the photo upload I was storing the image file in a temporary folder and then deleting the directory after moving the file to a permanent location. Apparently its default behaviour for ASP.Net to restart if a directory within the web site is deleted. I found this post
which describes the problem and offers a solution whereby the following code is added to the Global.asax.cs file. Implementing this solution has fixed the problem. The fix is applied by calling FixAppDomainRestartWhenTouchingFiles() from the Application_Start() event:
protected void Application_Start()
{
FixAppDomainRestartWhenTouchingFiles();
}
private void FixAppDomainRestartWhenTouchingFiles()
{
if (GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted)
{
/*
From: http://www.aaronblake.co.uk/blog/2009/09/28/bug-fix-application-restarts-on-directory-delete-in-asp-net/
FIX disable AppDomain restart when deleting subdirectory
This code will turn off monitoring from the root website directory.
Monitoring of Bin, App_Themes and other folders will still be
operational, so updated DLLs will still auto deploy.
*/
PropertyInfo p = typeof(HttpRuntime).GetProperty(
"FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
object o = p.GetValue(null, null);
FieldInfo f = o.GetType().GetField(
"_dirMonSubdirs", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase);
object monitor = f.GetValue(o);
MethodInfo m = monitor.GetType().GetMethod(
"StopMonitoring", BindingFlags.Instance | BindingFlags.NonPublic);
m.Invoke(monitor, new object[] { });
}
}
private AspNetHostingPermissionLevel GetCurrentTrustLevel()
{
foreach (AspNetHostingPermissionLevel trustLevel in
new AspNetHostingPermissionLevel[] {
AspNetHostingPermissionLevel.Unrestricted,
AspNetHostingPermissionLevel.High,
AspNetHostingPermissionLevel.Medium,
AspNetHostingPermissionLevel.Low,
AspNetHostingPermissionLevel.Minimal }
)
{
try
{
new AspNetHostingPermission(trustLevel).Demand();
}
catch (System.Security.SecurityException)
{
continue;
}
return trustLevel;
}
return AspNetHostingPermissionLevel.None;
}
Since sessions are associated with cookies, they are available for a specific domain.
It's a common mistake to ask for a session variable in the same application while the domain has changed (i.e. redirecting to a subdomain).
Does the controller action that you are posting the form contains any [Authorize] attribute. You need to post some code.
Verify a new session is really started every time. Check Trace output for the user's session id to ensure it realllly has changed.
Ensure the cookie getting sent over is actually getting set and sent over. (called ASPsessionIDSOMETHING ) and if that is being sent by the browser. Download the tool Fiddler to check the cookies easily (set cookie header coming from the server and the request cookies going back to the server from the browser. Make sure your browser is accepting the cookie and you dont say... have cookies turned off.
If your session id is changing at every request then your session isn't properly getting set the first time, set a break point on that code if you havent already.
You can log when the worker process resets - ensure that isn't the case. see http://www.microsoft.com/technet/prodtechnol/windowsserver2003/library/IIS/87892589-4eda-4003-b4ac-3879eac4bf48.mspx
I had the same problem. The problem only occured when a post request was send to the server and the session was not modified during that request. What I did as a workaround was, to write a custom filter which does nothing more than writing a key / value into the session on each request and added that filter to the GlobalFilter collection in the global.asax.
public class KeepSessionAlive : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
if(filterContext.HttpContext.Session != null)
{
filterContext.HttpContext.Session["HeartBeat"] = DateTime.Now.ToShortDateString();
}
}
public void OnActionExecuted(ActionExecutedContext filterContext) { }
}
And in the global.asax:
protected override void AddCustomGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new KeepSessionAlive());
}
This might be not the best solution but it helped me in my case.

Resources