ASP.net MVC - authentication cookie and redirects - asp.net-mvc

I am looking for a bit of a point in the right direction...
We have an MVC site, with a variety of virtual directories that all point at the same code, e.g.
https://www.x.com/dir1
https://www.x.com/dir2
The different virtual directories are used partly for business reasons due to the URL 'content' and partly to control how the site is skinned.
The sites are locked down for access using forms authentication, and I am trying to track down a slightly elusive issue.
A user logs into the site using the url 'dir1', authenticates fine, SetAuthCookie is called.
We have code that runs on OnActionExecuting throughout the site - it takes the logged in user and determines which virtual directory they should be accessing (one per user) and if they are in the wrong URL, will redirect them, e.g. (simplified code):
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (Authenticated && UserIsNotInCorrectDirectory())
{
filterContext.Result = new RedirectResult("https://www.x.com/dir2");
}
}
The problem I am having is this - if I start a fresh browser (using firefox at the minute, to view the cookies) and do the following:
Log into the site using the 'dir1' url.
I get authenticated fine - I can see the set-cookie http header containing our auth cookie. This response will also redirect me to 'dir2'.
On the subsequent page, when I view the cookies, the auth cookie is not there - this is the problem.
To add to my confusion, if I then bring up the login page again (same browser, session not closed), and try this again, it works. Anybody got a clue?

My bet is that your OnActionExecuting filter is getting in the way of your login form and your session cookie is getting lost when you overwrite the result. In case this code resides in an attribute, I would try removing the attribute from your Login action and see if that works.

Related

__RequestVerificationToken is not always being created

We have a page with several forms. Each has its own #Html.AntiForgeryToken(). On my local machine everything is great.
We deployed to Azure (PAAS), but the __RequestVerificationToken is not being created on every request. Sometime it is there and sometime I get the The required anti-forgery cookie is not present and rarely I get the tokens do not match error.
I'm completely clueless at this point. I can't figure out if there's something wrong in our code or on Azure environment? No ajax in these forms.
We have added the <machineKey> section to our web.config. No caching. Sometimes it occurs on new devices from the first time.
After spending a significant amount of time with investigation, using a combination of Sentry and Azure web server logs, I've found 2 major causes of the mentioned errors:
1) On mobile phones, when the browser is in the background, it may be abruptly stopped by the OS to free up resources. When this happens, usually, the page is stored on the phone's drive, and reloaded from there once the browser is re-opened.
The problem, however, is that by this time, the Anti-Forgery Token, which is a session cookie, has already expired, since this is essentially a new session. So the page loads without an Anti-Forgery Cookie, using HTML from the previous session. This causes the The required anti-forgery cookie is not present exception.
2) While seemingly related, the tokens do not match exception is usually only tangentially related. The cause seems to be user behaviour of opening multiple tabs at the same time.
The Anti-Forgery Cookie is only assigned when a user arrives to a page with a form on it. This means that they can go to your homepage, and have no anti-forgery cookie. Then they can open multiple tabs using middle-click. The multiple tabs are multiple parallel requests, each of them without an anti-forgery cookie.
As these requests don't have an anti-forgery cookie, for each of them, ASP.NET generates a separate pseudo-random token for their cookie, and uses that in the form; however, only the result of the last header received will be retained. This means that all the other pages will have invalid tokens on the page, since their anti-forgery cookie was overridden.
For a solution, I've created a global filter that should ensure that
The Anti-Forgery cookie is assigned on any page, even if the page has no form, and
The Anti-Forgery cookie is not session-bound. It's lifetime should be adjusted to match the user login token, but it should persist between sessions in case a mobile device reloads the page without the session.
The code below is a FilterAttribute that has to be added inside FilterConfig.cs as a global filter. Please do note that, while I do not believe this would create a security hole, I am by no means a security expert, so any input is welcome.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AntiForgeryFilter : FilterAttribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var cookie = filterContext.HttpContext.Request.Cookies.Get(AntiForgeryConfig.CookieName);
var addCookie = true;
if (string.IsNullOrEmpty(cookie?.Value))
{
cookie = filterContext.HttpContext.Response.Cookies.Get(AntiForgeryConfig.CookieName);
addCookie = false;
}
if (string.IsNullOrEmpty(cookie?.Value))
{
AntiForgery.GetTokens(null, out string cookieToken, out string _);
cookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
{
HttpOnly = true,
Secure = AntiForgeryConfig.RequireSsl
};
}
cookie.Expires = DateTime.UtcNow.AddYears(1);
if(addCookie) filterContext.HttpContext.Response.Cookies.Add(cookie);
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
I believe your problem comes from having multiple forms with different anti-forgery tokens on one page.
When page is requested you get two different tokens in forms hidden fields but only one token in the cookies.
Form POST contains mismatching tokens that causes an error.
Try how AFT will work for you if page contains only one form. If it works OK then my assumption is correct.
This answer contains possible solution for page with multiple forms however it has some security drawbacks as explained here.
I'm not sure why everything works OK on your localhost.
I've created simple application and tried form POST with correct cookies token but old Form token from previous session. To my surprise such POST successfully passes.
May be asp.net has some special handling for local requests in this case. I haven't found any info on this.
If my answer still doesn't help you could you please provide following data for further analysis:
Original page request with returned HTTP headers and form anti-forgery tokens.
Form POST request with sent HTTP headers.

ASP.NET MVC action not found on first request after AppDomain restart, when using [HttpGet]

I have a public ASP.NET MVC action that uses [HttpGet], like so:
[HttpGet]
public ActionResult List(OrganizationFilterModel p_model) {
...
}
If a user requests a page without a valid user session, we redirect them to the login page. The login system redirects them to the originally requested URL after logging them in.
On this page, under certain circumstances, the redirect from the login system to the original URL results in an exception being thrown:
A public action method "List" was not found on controller "MyApp.Controllers.OrganizationController". However, if I simply re-request the page, it displays successfully then and for all subsequent requests.
Conditions and other notes:
The user has a valid FormsAuth ticket. [Meaning, they were recently logged in]
The user's session is gone, due to expiration or an AppDomain reset.
Removing the [HttpGet] fixes the problem. [Even though the failed request WAS a GET]
Under these conditions, the existence of the [HttpGet] filter prevents MVC from finding the action, but only on the first request. All subsequent requests work fine.
Fiddler shows the following pattern:
GET to /Organization/List --> returns 302 to login page
GET for /Account/Login
POST to /Account/Login [user logs in, then gets redirected]
GET for /Organization/List --> throws error
GET for /Organization/List --> if I repeat the request, it works fine
Summary: the [HttpGet] filter is preventing the action from being located, but only on the first request and despite the fact that it is a GET request.
What's going on?
Update:
To add some additional information:
Using MVC 4
We use FormsAuth but do NOT use the [Authorize] attribute. We have custom code in OnActionExecuting that forces a login if the request is authenticated, but doesn't have a valid session. (We really just use FormsAuth for some of the static convenience methods and configuration settings)
The issue DOES NOT occur if FormsAuth causes the redirect to the login page. The issue DOES occur if our custom code causes the redirect.
Removing FormsAuth altogether fixes the issue as well.
Since removing FormsAuth fixes the issue, and we've wanted to do that for awhile, I'm ceasing troubleshooting. I'd really love to know if this is a defect in FormsAuth + MVC [or FormsAuth + Routing], or if I've done something stupid.

Custom forms authentication using login from parent domain

I have an parent MVC site that handles logins that has the domain mysite.com. This is basically the template MVC internet application out of the box - a user logs in, and it sets an .ASPXAUTH cookie with the domain .mysite.com.
I also have another MVC site that runs on the domain child.mysite.com. I intend to use custom forms authentication to authenticate the user from the cookie set by the parent. When I browse to child.mysite.com in Firefox, I can see the cookie set by the login site in Firebug, so I know the child site can access it, but I do not seem to be able to retreive this cookie from my code in the child site.
I am implementing FormsAuthentication_OnAuthenticate in Global.asax, and I would have expected the cookie to be visible in Request.Cookies, but there are no cookies there.
How do I access the cookie set by the parent login site in FormsAuthentication_OnAuthenticate?
I think I've found the problem. The child site was finding the cookie, but when it tried to decrypt it, it was erroring, and therefore not authenticating fully. This code on the child site would thow and error:
void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs e)
{
var cookie = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value);
}
The error is 'Padding is invalid and cannot be removed' and is due to the fact that the child site cannot decrypt the cookie set by the parent site.
The answer is to set the machine key in system.web in web.config to be the same for both sites:
<machineKey
validationKey='241FF35BE3921690EBA492A89CC03719ECF5552D019448C44F8B28B01F546FCDC4AEDCD273380EB45BE8A49AFB9C14FE60BECF0B5ECBA4901C306875FED98DEA'
decryptionKey='864559FC58AC5FFB5B9581008552B4A873ACBE86469A81CB'
validation='SHA1'/>

MVC's ActionExecutingContext HttpContext.User.Identity.IsAuthenticated Returns False When Signing in on Multiple Browser Tabs

During a custom ActionFilterAttribute's OnActionExecuting method, we ensure that the user is still logged in before performing some actions. We do this by doing something similar to this pseudo code:
public override void OnActionExecuting( ActionExecutingContext filterContext )
{
if ( filterContext.HttpContext.User.Identity.IsAuthenticated )
{
// Do something...
}
}
I have multiple sites for multiple clients that run under the same domain with only difference being the virtual directory names. Each virtual directory actually points to same folder/code base and the URL/virdir name indicate to code which 'client configuration file' to use from a nested /Clients directory. Not sure if that much detail in site/code/IIS config is needed, but supplying in case any of that is culprit for problem.
If I try to sign on to multiple sites using multiple instances of a browser, everything works fine. The IsAuthenticated check returns true when I attempt to navigate around the site.
However, if I try to sign on to multiple sites using a single browser with multiple tabs, I keep getting logged out back and forth. If I sign in to site A, I can navigate around, but as soon as I sign into site B, if I try to navigate anywhere in site A, IsAuthenticated returns false.
Is this expected behavior? Is there a workaround to this?
UPDATE: I'm now only able to reproduce this behavior in IE. In Firefox and Chrome, I get booted to login screen whether I'm on same browser/multi tabs or multi browsers. Is there a difference in the way IE handles cookies? Or aren't cookies the culprit?
Without knowing your setup in any more detail, this is what I expect is happening.
Assumptions:
You state that you have multiple virtual directories pointing to one code base.
Each of these virtual directories are most likely set as an application is IIS.
You do not have a machine key defined in your web.config and as a result, each virtual directory auto-generated its own encryption/decryption keys
What is probably happening:
When you sign in from different browsers, each browser is given an authentication cookie. Since you are using different browsers, there is no issue.
When using the same browser, you login to site A and are given an encrypted cookie that was encrypted with the siteA autogenerated key.
When you attempt to go to another virtual directory that has a different autogenerated machine key, the site cannot read the authentication ticket (cannot decrypt it) and thus returns logged-in = false.
Once you login to siteB, the authentication cookie is replaced with an authentication ticket from siteB. At this point, siteA can no longer decrypt the authentication ticket and returns logged-in =false.
Try setting the machine key configuration section of your web.config with the appropriate options (MSDN on machineKey element). Here is some more information on the forms authentication ticket and process as well

Session not completely abandoned

I'm having an odd problem with my sessions. I haven't found a solution even after searching through the internet for a couple of days and I find it really strange that nobody else seems to be having this problem, which makes me think that it could be something I'm doing wrong.
If I log onto my website and visit a page decorated with the [Authorize] attribute (i.e. it requires the user to have logged on in order to access it), save the request with fiddler, log out and re-issue the request with fiddler one would expect that to fail (or re-direct me to the login page at least) but no, it returns the page as if I were still logged in.
When I'm logging out this is what I do (I'm using asp.net forms authentication)
FormsAuthentication.SignOut();
Session.Clear();
Session.Abandon();
Session.RemoveAll();
Response.Cookies.Add(new HttpCookie("ASP.NET_SessionId",""));
That should work... but it doesn't.
In case anyone is wondering this is what I am using:
IIS 7.5
MVC 3.0
.NET 4.0
If I log onto my website and visit a page decorated with the [Authorize] attribute (i.e.
it requires the user to have logged on in order to access it), save
the request with fiddler, log out and re-issue the request with
fiddler one would expect that to fail
Why would you expect something like this? Forms authentication uses cookies to track logged in users. When you do FormsAuthentication.SignOut(); all that happens is that you remove the authentication cookie so that on subsequent requests from this browser the cookie is no longer sent and the server thinks that the user is not logged in. If on the other hand you have captured a valid cookie with Fiddler, you are perfectly capable of sending a valid request to the server (in the time for which this cookie is valid obviously). There is nothing on the server for it to know that you used fiddler, right? It's completely stateless.
If you wanted to avoid this behavior (which is by design) you will have to keep track somewhere on the server either a list of online users, or add some flag in the database about the user which indicates whether he is online or not. Then you will write a custom Authorize attribute that will check in this centralized store whether the user is allowed or not to login. When he logs out you will change this flag. Obviously if he closes his browser you won't be able to change the flag but if he signs out normally you will know that even if someone had captured the authentication cookie, he will not be able to use it to issue a valid request.

Resources