Our security team requires that all the cookies are set to Secure=true.
To set the secure property for MVC AntiForgery, we are using the following code:
protected void Application_BeginRequest(object sender, EventArgs e)
{
AntiForgeryConfig.RequireSsl = HttpContext.Current.Request.IsSecureConnection;
}
But now we have a problem on our test server which is not using SSL. Sometimes we have spontaneous errors
The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.
When looking in the ASP.NET MVC code to pinpoint the location of the exception, we found the following
private void CheckSSLConfig(HttpContextBase httpContext)
{
if (_config.RequireSSL && !httpContext.Request.IsSecureConnection)
{
throw new InvalidOperationException(WebPageResources.AntiForgeryWorker_RequireSSL);
}
}
It seems correct and it should work because the execution sequence is
AntiForgeryConfig.RequireSsl = HttpContext.Current.Request.IsSecureConnection;
// ... something happens in between
if (_config.RequireSSL && !httpContext.Request.IsSecureConnection)
{
throw new InvalidOperationException(WebPageResources.AntiForgeryWorker_RequireSSL);
}
But it seems that for some requests HttpContext.Current.Request.IsSecureConnection is returning true although we are not using SSL on our test server.
What's going on there? Why do we get this exception?
I was searching for information about AntiForgeryConfig.RequireSsl and found your question.
In your following code:
protected void Application_BeginRequest(object sender, EventArgs e)
{
AntiForgeryConfig.RequireSsl = HttpContext.Current.Request.IsSecureConnection;
}
You modify an application level value (AntiForgeryConfig.RequireSsl) with a local value (Request.IsSecureConnection).
If you have 2 requests with different Request.IsSecureConnection value, what do you think will happen ?
- first request set AntiForgeryConfig.RequireSsl to false
- second request set AntiForgeryConfig.RequireSsl to true
- first request is evaluated by CheckSSLConfig (true)
- second request is evaluated by CheckSSLConfig (true)
You must avoid modifying global application setting this way and write your own filter to handle that kind of behavior.
Related
I'm in the process of upgrading a legacy app that I inherited from MVC3 to MVC5 and somehow I broke the HTTPContext.User object.
The app has a custom authentication mechanism that seems to be working correctly as it returns a proper User object to the rest of the pipeline. Specifically, in Global.asax.cs:
protected void Application_BeginRequest(object sender, EventArgs args)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.GetType().FullName.Equals("Citation.AMS.Users.UserContext", StringComparison.OrdinalIgnoreCase))
{
try
{
using (IAuditor auditor = CoreFactory.AuditorCreate())
{
auditor.WriteAudit(HttpContext.Current, "Compliance");
}
}
catch (Exception ex)
{
using (ILogger log = CoreFactory.LoggerCreate())
{
log.WriteFatalError("Exception in Application_BeginRequest::Audit.", ex);
}
}
The check for HTTPContext.Current.User returns the correct object above. But by the very next method call:
protected void Application_AcquireRequestState(object sender, EventArgs args)
{
CultureInfo ci = null;
HttpCookie cookie = Request.Cookies[CookieHelper.CookieName];
if (cookie != null && !string.IsNullOrWhiteSpace(cookie.Values[CookieHelper.CultureName]))
{
ci = new CultureInfo(cookie.Values[CookieHelper.CultureName]);
}
else
{
The object is converted then to an RolePrincipal instead of the correct User type.
There must be some processing done between those two method calls in the pipeline but I can't figure out what's happening between them that would change the user object.
In the original code base, this doesn't happen. I've also created a new branch based on the MVC3 branch and upgraded all the nuget packages and updated the web.config files as I found issues and that seems to work better (don't want to go that route because my other branch has a lot of other changes that would be challenging to replicate.)
Can someone tell me what I'm missing or how to find out why the object is changing between these method calls?
Found it, it was custom javascript that was redirecting my controllers.
On our MVC website, we secure our endpoints with the Authorize attribute, however I would like to redirect the user to the login screen when this occurs. My first solution was to redirect on Application_EndRequest in Global.asax.cs:
protected void Application_EndRequest(object sender, EventArgs e)
{
if (Response.StatusCode == (int) HttpStatusCode.Unauthorized)
{
Response.ClearContent();
Response.Redirect("/Account/Login");
}
}
The problem with this is that some of our views are loaded by AJAX, in which case we do not want to show the login screen within dynamically-loaded div (this happens for example when the back button is pressed after logging out). It would be better if the request still returned the login screen, but with a 401 Unauthorized status code so that we can detect this event. Obviously this will not work with this approach because the redirection relies on a 302 status code.
Instead, I would rather 'insert' the login screen content inside Application_EndRequest with a 401 status code. So, instead of the Response.Redirect, I tried this:
...
Response.ClearContent();
Server.TransferRequest("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
...
The redirect works nicely, but because this performs a new request, I can't set the status code in this way, and I get a 200. Still no good for detecting an unauthorized request.
I also tried:
...
Response.ClearContent();
Server.Transfer("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
...
But I get an exception "Error executing child request for /Account/Login."
I also tried:
...
Response.ClearContent();
HttpContext.Current.RewritePath("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
Response.End();
...
But this throws an exception "Thread was being aborted". I'm not sure if the Response.End() is necessary but without it, I just get a standard IIS 401 error.
How can I return the content of my login screen but with a 401 status code?
Sending a 401 AND a ViewObject or RedirectObject is not how the MVC Framework was designed to work: -- a few posts have cleverly shown that returning a 401 with a View witn an authorization filter..
I specify the MVC Framework, but this is not even the prefered way to use the HTTP Protocol.
When you redirect in your MVC application, you have created a RedirectResult
When your MVC application returns with a call to the Controller's View() method, a ViewResult is created
Whether a Redirect or a View eventually reaches the client browser either,
an HTTP Return Code of 200 -- Success will be returned
Or
an HTTP Return Code of 302 -- Redirect will be returned
The reason for this convention is that, the server is "handling" the redirect. If the user is not authorized, the server will redirect the user to the login page.
Simlarly, in AJAX or asynchronous HTTP requests, so long as an HTML View is returned, the HTTP request is considered a success. Redirect (302) is not returned for asynchronous requests.
Sending Error Codes to client:
If you want to return a 401 unauthorized to the client, that usually means that you want to the client to "handle" the unauthorized request by navigating to the Login or Error View.
$.ajax({
url: loginURL,
data: $("#form").serialize(),
type: "POST",
dataType: "html"
})
.success(function (result, status) {
/*additional logic*/
})
.error(function (xhr, status) {
if (xhr.status == 401)
{
window.location = unauthorizedUrl; //redirect to unauthorized url
}
});
What if you want to Redirect to Login Page AND Display an Error
your question makes most sense if you want to redirect to a Login Page AND display an Error
Then, the convention is to Add your error message into Viewbag or a ModelState Error
such as:
var user = await signInManager.UserManager.FindByNameAsync(model.UserName);
ModelState.AddModelError("", "Invalid username or password.");
return View(model);
In your View, to have code that looks for an error:
A ValidationSummary would pickup the Model error
#Html.ValidationSummary(true)
This specific example of Bad Login Attempt doesn't exactly speak to your example.
I couldn't find an image for a user redirected because they were not authorized, but, in this case, the logic is similar. Error Msg can be populated with a QueryString, Viewbag, etc.
You would ideally want to use a custom AuthorizeAtrribute and then hook into the HandleUnauthorizedRequest method and from that, return a view to your login page by setting the controller and view to call in the AuthorizationContext.
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.RouteData.Values["controller"] = "home";
filterContext.RouteData.Values["action"] = "login";
filterContext.Result = new ViewResult ();
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
}
}
note: The trick for this is to make sure you set the following value to true to avoid forms authentication taking over the response once you've set the 401 status.
HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
I tried quite a few different methods all of which had their own sneaky problems. Not sure how your solution is setup but if you need to perform a transfer you can use the following:
Custom AuthorizeAttribute
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.RequestContext.HttpContext.Server.TransferRequest("/Account/Login/?NOT_AUTHORISED=TRUE", false);
}
}
You need to pass a message to the next request, using the QueryString seemed to do the trick. Although you need to remember that anyone can set a QS parameter.
Set Status Code
protected void Application_EndRequest(object sender, EventArgs e)
{
if (Request["NOT_AUTHORISED"] == "TRUE")
{
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
}
Demo
There's no redirect and the HTTP Status code is set to 401.
In my ASP .NET MVC 2 - application, there are several controllers, that need the session state. However, one of my controllers in some cases runs very long and the client should be able to stop it.
Here is the long running controller:
[SessionExpireFilter]
[NoAsyncTimeout]
public void ComputeAsync(...) //needs the session
{
}
public ActionResult ComputeCompleted(...)
{
}
This is the controller to stop the request:
public ActionResult Stop()
{
...
}
Unfortunately, in ASP .NET MVC 2 concurrent requests are not possible for one and the same user, so my Stop-Request has to wait until the long running operation has completed. Therefore I have tried the trick described in this article and added the following handler to Global.asax.cs:
protected void Application_BeginRequest()
{
if (Request.Url.AbsoluteUri.Contains("Stop") && Request.Cookies["ASP.NET_SessionId"] != null)
{
var session_id = Request.Cookies["ASP.NET_SessionId"].Value;
Request.Cookies.Remove("ASP.NET_SessionId");
...
}
}
This simply removes the session-id from the Stop-Request. At the first glance this works well - the Stop-Request comes through and the operation is stopped. However, after that, it seems that the session of the user with the long running request has been killed.
I use my own SessionExpireFilter in order to recognize session timeouts:
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContext ctx = HttpContext.Current;
// check if session is supported
if (ctx.Session != null)
{
// check if a new session id was generated
if (ctx.Session.IsNewSession)
{
// If it says it is a new session, but an existing cookie exists, then it must
// have timed out
string sessionCookie = ctx.Request.Headers["Cookie"];
if ((null != sessionCookie) && (sessionCookie.IndexOf("ASP.NET_SessionId") >= 0))
{
filterContext.Result = new JsonResult() { Data = new { success = false, timeout = true }, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
}
}
base.OnActionExecuting(filterContext);
}
}
ctx.Session.IsNewSession is always true after the Stop-Request has been called, but I don't know why. Does anyone know why the session is lost? Is there any mistake in the implementation of the Stop-Controller?
The session is lost because you removed the session cookie. I'm not sure why that seems illogical. Each new page request supplies the cookie to asp.net, and if there is no cookie it generates a new one.
One option you could use to use cookieless sessions, which will add a token to the querystring. All you need to do is generate a new session for each login, or similar.
But this is one of the reasons why session variables are discouraged. Can you change the code to use an in-page variable, or store the variable in a database?
I'm facing a strange problem with ASP.NET MemoryCaching in a MVC 3 ASP.NET application.
Each time an action is executed, I check if its LoginInfo are actually stored in the MemoryCache (code has been simplified, but core is as follow):
[NonAction]
protected override void OnAuthorization(AuthorizationContext filterContext) {
Boolean autorizzato = false;
LoginInfo me = CacheUtils.GetLoginData(User.Identity.Name);
if (me == null)
{
me = LoginData.UserLogin(User.Identity.Name);
CacheUtils.SetLoginInfo(User.Identity.Name, me);
}
// Test if the object is really in the memory cache
if (CacheUtils.GetLoginData(User.Identity.Name) == null) {
throw new Exception("IMPOSSIBLE");
}
}
The GetLoginInfo is:
public static LoginInfo GetLoginData(String Username)
{
LoginInfo local = null;
ObjectCache cache = MemoryCache.Default;
if (cache.Contains(Username.ToUpper()))
{
local = (LoginInfo)cache.Get(Username.ToUpper());
}
else
{
log.Warn("User " + Username + " not found in cache");
}
return local;
}
The SetLoginInfo is:
public static void SetLoginInfo (String Username, LoginInfo Info)
{
ObjectCache cache = MemoryCache.Default;
if ((Username != null) && (Info != null))
{
if (cache.Contains(Username.ToUpper()))
{
cache.Remove(Username.ToUpper());
}
cache.Add(Username.ToUpper(), Info, new CacheItemPolicy());
}
else
{
log.Error("NotFound...");
}
}
The code is pretty straightforward, but sometimes (totally randomly), just after adding the LoginInfo to the MemoryCache, this results empty, the just added Object is not present, therefore I got the Exception.
I'm testing this both on Cassini and IIS 7, it seems not related to AppPool reusability (enabled in IIS 7), I've tested with several Caching policies, but cannot make it work
What Am I missing/Failing ?
PS: forgive me for my bad english
Looking at the code for MemoryCache using a decomplier there is the following private function
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs eventArgs)
{
if (!eventArgs.IsTerminating)
return;
this.Dispose();
}
There is an unhandled exception handler setup by every MemoryCache for the current domain Thread.GetDomain() so if there is ever any exception in your application that is not caught which may be common in a website it disposes the MemoryCache for ever and cannot be reused this is especially relevant for IIS apps as apposed to windows applications that just exit on unhanded exceptions.
The MemoryCache has limited size. For the Default instance, is't heuristic value (according to MSDN).
Have you tried to set Priority property on CacheItemPolicy instance to NotRemovable?
You can have race-condition because the Contains-Remove-Add sequence in SetLoginInfo is not atomic - try to use Set method instead.
Btw. you are working on web application so why not to use System.Web.Caching.Cache instead?
I believe you are running into a problem that Scott Hanselman identified as a .NET 4 bug. Please see here: MemoryCache Empty : Returns null after being set
I'm looking at using ELMAH for the first time but have a requirement that needs to be met that I'm not sure how to go about achieving...
Basically, I am going to configure ELMAH to work under asp.net MVC and get it to log errors to the database when they occur. On top of this I be using customErrors to direct the user to a friendly message page when an error occurs. Fairly standard stuff...
The requirement is that on this custom error page I have a form which enables to user to provide extra information if they wish. Now the problem arises due to the fact that at this point the error is already logged and I need to associate the loged error with the users feedback.
Normally, if I was using my own custom implementation, after I log the error I would pass through the ID of the error to the custom error page so that an association can be made. But because of the way that ELMAH works, I don't think the same is quite possible.
Hence I was wondering how people thought that one might go about doing this....
Cheers
UPDATE:
My solution to the problem is as follows:
public class UserCurrentConextUsingWebContext : IUserCurrentConext
{
private const string _StoredExceptionName = "System.StoredException.";
private const string _StoredExceptionIdName = "System.StoredExceptionId.";
public virtual string UniqueAddress
{
get { return HttpContext.Current.Request.UserHostAddress; }
}
public Exception StoredException
{
get { return HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] as Exception; }
set { HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] = value; }
}
public string StoredExceptionId
{
get { return HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] as string; }
set { HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] = value; }
}
}
Then when the error occurs, I have something like this in my Global.asax:
public void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var item = new UserCurrentConextUsingWebContext();
item.StoredException = args.Entry.Error.Exception;
item.StoredExceptionId = args.Entry.Id;
}
Then where ever you are later you can pull out the details by
var item = new UserCurrentConextUsingWebContext();
var error = item.StoredException;
var errorId = item.StoredExceptionId;
item.StoredException = null;
item.StoredExceptionId = null;
Note this isn't 100% perfect as its possible for the same IP to have multiple requests to have errors at the same time. But the likely hood of that happening is remote. And this solution is independent of the session, which in our case is important, also some errors can cause sessions to be terminated, etc. Hence why this approach has worked nicely for us.
The ErrorLogModule in ELMAH (version 1.1 as of this writing) provides a Logged event that you can handle in Global.asax and which you can use to communicate details, say via HttpContext.Items collection, to your custom error page. If you registered the ErrorLogModule under the name ErrorLog in web.config then your event handler in Global.asax will look like this:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var id = args.Entry.Id
// ...
}