"Down for Maintenance" page for Asp.net MVC web app - asp.net-mvc-3

I have the following requirements
Set a value in web.config and enable maintenance mode
All non-ajax requests should be shown a custom error page, with the http status code set to 503. The url of page should be retained.
All ajax requests should be responded with http status code 503
I should have an opportunity to do some basic logging to a file. Log the url and the user Identity if he happened to be logged into the app
I am using ELMAH to track/log all unhandled exceptions. The mechanism for implementing maintenance mode shouldn't need me to not use ELMAH
I have "runAllManagedModulesForAllRequests" set to true. this was originally done for use with RequestReduce. we no longer use it, but I am hesitant to reset its value to false. I am not sure if any other library needs it.
Once I realized there is nothing built in which supports the above requirements, I felt I had the following two options in front of me (App_offile.html won't work for me).
an HttpModule
an MVC ActionFilter
I dropped the MVC ActionFilter as I couldn't figure out how to guarantee it to run before any authentication/authorization filters. I have a custom authentication filter which hits the db. The idea behind the maintenance mode is the db might be offline, yet the web-app shouldn't show a 500 custom error page, but a 503 custom error page.
I wrote the following httpmodule and added in my web.config. It works for ajax requests. It kinda works for non-ajax requests. All requests get redirected to the 503 error page. The side-effect is all requests for static content also result in a 503. My error page thus is shown unstyled :(
// the http module
public class MaintenanceModeModule : IHttpModule
{
private static bool _isUnderMaintenance;
static MaintenanceModeModule()
{
var valueStr = (ConfigurationManager.AppSettings["UnderMaintenance"] ?? (false).ToString());
bool underMaintenance;
bool.TryParse(valueStr, out underMaintenance);
_isUnderMaintenance = underMaintenance;
}
public void Init(HttpApplication application)
{
application.BeginRequest += OnBeginRequest;
}
private void OnBeginRequest(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var request = application.Request;
var response = application.Response;
if (_isUnderMaintenance == false)
{
return;
}
application.Context.Items["under_maintenance"] = true; // used later
if (request.Url.PathAndQuery == "/503") // the url of the action that renders the custom error page
{
return;
}
const int statusCode = (int) HttpStatusCode.ServiceUnavailable;
const string statusMessage = "Temporarily down for maintenance";
var requestWrapper = new HttpRequestWrapper(request);
if (requestWrapper.IsAjaxRequest())
{
response.Clear();
response.ClearContent();
response.ClearHeaders();
response.StatusCode = statusCode;
response.TrySkipIisCustomErrors = true;
response.StatusDescription = statusMessage;
response.End();
return;
}
// doesn't work, shows the Yellow Screen of Death (YSoD)
// application.Context.Server.Transfer("~/503", preserveForm: true);
// doesn't work, shows the Yellow Screen of Death (YSoD)
// throw new HttpException(statusCode, statusMessage);
response.Redirect("~/503");
}
public void Dispose()
{
}
}
...
// web.config
// only the relevant portions of each section is shown
<appSettings>
<add key="UnderMaintenance" value="true" />
</appSettings>
<customErrors mode="On"> <!-- Custom errors are on, even then I was seeing YSoDs during my attempts -->
<error statusCode="404" redirect="404" />
<error statusCode="503" redirect="503" />
</customErrors>
<system.webServer>
<httpErrors existingResponse="PassThrough">
</httpErrors>
<modules runAllManagedModulesForAllRequests="true">
<add name="MaintenanceMode" type="WebApp.Code.MvcInfrastructure.MaintenanceModeModule" />
</modules>
</system.webServer>
...
// route config
routes.MapRoute("503Error", "503", new { controller = "Error", action = "UnderMaintenance" });
...
// error controller
// the authentication filter skips authentication if the allowanonymous attribute is present
[AllowAnonymous]
public class ErrorController : CustomBaseController
{
public ErrorController(AppConfig appConfig)
: base(appConfig)
{
}
public ActionResult UnderMaintenance()
{
// behind the scenes reads the value from HttpContext.Items.
// This was set during the execution of the httpmodule
if (AppConfig.UnderMaintenance == false)
{
return new RedirectResult("~/");
}
Response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
Response.TrySkipIisCustomErrors = true;
// the actual content of the view is not relevant now
return View("Error503");
}
}
The problems with this approach,
Each non-ajax request is responded with a 302 and then a 503
The URL requested by the browser is not retained
It returns a 503 for all static assets as well
The code I wrote and web.config settings I enabled are all cobbled together from various sources. I am not fully sure what those settings do or what the recommended way is. Please feel free to answer with a completely different method, as long as it can meet the requirements stated.

Related

Handle Ajax calls when user session expires

In my MVC 5 web application when the user session expires and the user clicks in something in the page that does an AJAX call I was still getting response status 200 with X-Responded-JSON:401.
I have then read and investigated and found a way to solve the problem.
So...added to the Startup class ConfigureAuth method this
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider =
new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(TimeSpan.FromMinutes(15), (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
},
SlidingExpiration = false,
ExpireTimeSpan = TimeSpan.FromMinutes(30)
});
Just a helper...
private static bool IsAjaxRequest(IOwinRequest request)
{
IReadableStringCollection query = request.Query;
if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
{
return true;
}
IHeaderDictionary headers = request.Headers;
return (headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest");
}
This solved my problem locally (in my dev machine) and I am now able to see the 401 in the Ajax calls when the sessions expires or the user logs or when is still trying to access something in a open tab.
My problem is that this work only locally. When I have my application in a server this Ajax calls still return 200 and I do not understand why....however if I access the application inside of the box everything works as expected and I can see the 401 in the browser
Can someone maybe know a better solution for this or maybe the reason why this works locally but not in the server?
Many thanks
The solution posted in the initial question works as expected. This approach was not working in the beginning because of the custom error pages that you can define in the web.config. It was messing up with the initial solution that I found in the web.
<system.web>
<globalization enableClientBasedCulture="true" culture="en-GB" uiCulture="auto" />
<customErrors mode="Off" defaultRedirect="~/Error/Index">
<error statusCode="400" redirect="~/Error/Index" />
<error statusCode="401" redirect="~/Error/Index" />
...
...
This was redirecting to a different page and because of that was returning 200 instead of 401.
Thanks

keep getting The view "Error" not found when using Elmah and asp.net mvc 4

I am using Elmah 1.2 as the logging framework for my asp.net mvc 4 application.
in the web.config file, I set customErrors mode to on.
<customErrors mode="On" defaultRedirect="/Error">
<error statusCode="404" redirect="/Error/NotFound" />
</customErrors>
I also created a custom HandleErrorAttribute, copied the code from this link.
http://joel.net/logging-errors-with-elmah-in-asp.net-mvc-3--part-4--handleerrorattribute
In my Home controller, i just throw an exception to test the logging framework.
public ActionResult About()
{
throw new Exception("this is a buggggggggggggg");
ViewBag.Message = "Your app description page.";
return View();
}
"this is a buggggggggggggg" is logged in the database, great, it works. then there's another error also logged, and I didnt expect that to happen.
The view 'Error' or its master was not found or no view engine supports the searched locations. The following locations were searched: ~/Views/Home/Error.aspx ~/Views/Home/Error.ascx ~/Views/Shared/Error.aspx ~/Views/Shared/Error.ascx ~/Views/Home/Error.cshtml ~/Views/Home/Error.vbhtml ~/Views/Shared/Error.cshtml ~/Views/Shared/Error.vbhtml
Update:
follow Tim's suggestion, then it causes another issue.
If I create a Error.cshtml in the shared folder. when unhandled exception happens, it will show this Error.cshtml file, not "/Error" page. I have customErrors enabled. They should all get redirected to "/Error" page.
We created an empty MVC5 app and added ELMAH to it. We also were receiving the extra error you described even though we did not add the HandleErrorAttribute. After some research I found the nuget package Elmah.MVC which adds some additional configuration settings. In the appSettings section of web.config you will find these 2 lines:
<appSettings>
<add key="elmah.mvc.disableHandler" value="false" />
<add key="elmah.mvc.disableHandleErrorFilter" value="false" />
</appSettings>
These 2 keys default to "false". I changed their values to "true" and the extra logged exception went away.
I am developing an application using ASP.NET MVC 5 RC and I use Elmah too for error logging. I am using too a custom error handling attribute to redirect errors to a custom action on a custom controller, but mine doesn't look like the one shown in the link you provided.
However I had the same problem: Elmah was properly logging the error, but was also adding a "Error view not found" entry. I solved this by adding the following line to the OnException method on the attribute:
filterContext.ExceptionHandled = true;
For completeness, this is the complete code for the custom error handling attribute I am using:
public class CustomHandleErrorAttribute: HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
if(filterContext.HttpContext.Request.IsAjaxRequest()) {
filterContext.HttpContext.Response.StatusCode =
(int)HttpStatusCode.InternalServerError;
filterContext.Result = new ContentResult() {
Content = "Server error",
ContentType = "text/plain"
};
}
else {
filterContext.Result = new RedirectToRouteResult(
"Default",
new System.Web.Routing.RouteValueDictionary(new
{
controller = "Error",
action = "ApplicationError"
}));
}
}
}

MVC Generic error view and Ajax Post and error code 500

I have set up an mvc app with an _error.cshtml that is set to catch exceptions I throw in the controller.
I also have a few ajax posts on some pages that checks for errors and then it does something else.
On the server, I have a filter on all exceptions and then check if it is an ajax request and return something that can be deserialized on the client. The problem is that if I do not set the post response status code to 500 then ajax will not see this error and I can't show a nice message. If I set the status to 500 I get the default IIS error message stating something happened on the server.
I would like to handle some errors on the page in the ajax results but maintain the generic error handling. Is this an IIS setting to allow custom 500 message per site? The web.config Custom Error On|Off makes no difference in my case.
The filter you have on all exceptions that is checking if its an ajax request, is that a filter made on your own?
I had a slightly similar issue, and I had to make sure the flag TrySkipIisCustomErrors was set as true in order to avoid the standard IIS error.
This flag is located on the Response object of the HttpContext.
This is also done by the standard HandleError filter, pay attention to the last line in its implementation of the OnException method:
public virtual void OnException(ExceptionContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction) {
return;
}
// If custom errors are disabled, we need to let the normal ASP.NET exception handler
// execute so that the user can see useful debugging information.
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) {
return;
}
Exception exception = filterContext.Exception;
// If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
// ignore it.
if (new HttpException(null, exception).GetHttpCode() != 500) {
return;
}
if (!ExceptionType.IsInstanceOfType(exception)) {
return;
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult {
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
// Certain versions of IIS will sometimes use their own error page when
// they detect a server error. Setting this property indicates that we
// want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

IIS Overwriting HTTP Response Text When HTTP Response Status set as 400

I am building an MVC 3 application with an IIS 7.5 backend. On my controller, I have action methods that allow the user to add/edit domain objects. The Action handles HTTP Post, has a return value of string which contains any validation error messages encountered during the save process. Here is an example of one Action Method:
[HttpPost]
public string CustomerEdit(CustomerModel customerModel)
{
var errorMessages = new StringBuilder();
var serverErrors = new List<string>();
//Map to a customer domain object
Mapper.CreateMap<CustomerModel, Customer>();
var customer = Mapper.Map<CustomerModel, Customer>(customerModel);
if (customerModel.Oper == "del")
{
var customerIds = new List<Guid>();
customerIds.Add(customer.Id);
if (!_serverService.DeleteCustomers(customerIds))
{
errorMessages.Append("The item could not be deleted");
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
else
{
//Validate
if (!_serverService.ValidateCustomer(customer, out serverErrors))
{
foreach (var error in serverErrors)
{
ModelState.AddModelError("Validation", error);
}
}
//Save
if (ModelState.IsValid)
{
var saveStatus = _serverService.SaveCustomer(ref customer, _id);
if (!saveStatus)
{
errorMessages.Append("The server encountered and error during Save");
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
else
{
errorMessages.Append(GetValidationErrors(ModelState));
Response.StatusCode = Constants.STATUS_SERVER_ERROR;
}
}
return errorMessages.ToString();
}
In the case of an error, I need to set the Response.StatusCode property to a value of either 400/500, and return a concatenated string of detailed error messages. Unfortunately, IIS always strips my error string out of the response test, and (in the case of 400 errors) adds replaces it with the string 'Bad Request'
Is there a way to configure IIS to return a custom, Action-specific, string when the status code is set to 400?
After talking to a friend of mine who is a wiz at configuring IIS, I found that in IIS 7+ you can add the following to web.config:
<system.webServer>
<httpErrors errorMode="Detailed"/>
</system.webServer>
If this setting in web.config is used, AND you set the body of the response, then the response body will reach the client. If you do NOT set the response body, then IIS will serve up a detailed error page with detailed error information (see http://learn.iis.net/page.aspx/267/how-to-use-http-detailed-errors-in-iis/). Many folks consider this a security risk, so use with caution.
In MVC it is also possible to do this on a Action by Action basis. See TrySkipisCustomErrors.
Use:
Response.TrySkipIisCustomErrors = true;
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return this.Json(SomeJsonObject); // could output string too
we ended up with going for
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="PassThrough" />
</system.webServer>
in the web.config. Which allowed custom errors that set status codes to leave IIS unmolested.
At least the page Configuring HTTP Error Responses in IIS 7 says
Note
You cannot customize the following HTTP error messages: 400,
403.9, 411, 414, 500, 500.11, 500.14, 500.15, 501, 503, and 505.
EDIT: though in the responses of this question, which looks quite similar, there is a response claiming that at least something can be done with httpErrors configuration.

Configuring Windows Identity Foundation from code

I'm experimenting with "configuration-less WIF", where I want to accept a SAML2 token that is generated by Windows Azure's AppFabric STS.
What I'm doing is parsing checking the current request for token information, like so:
if (Request.Form.Get(WSFederationConstants.Parameters.Result) != null)
{
SignInResponseMessage message =
WSFederationMessage.CreateFromFormPost(System.Web.HttpContext.Current.Request) as SignInResponseMessage;
var securityTokenHandlers = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
XmlTextReader xmlReader = new XmlTextReader(
new StringReader(message.Result));
SecurityToken token = securityTokenHandlers.ReadToken(xmlReader);
if (token != null)
{
ClaimsIdentityCollection claims = securityTokenHandlers.ValidateToken(token);
IPrincipal principal = new ClaimsPrincipal(claims);
}
}
The code above uses the SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(); colection to verify and handle the SAML token. However: this does not work because obviously the application has not bee nconfigured correctly. How would I specify the follwing configuration from XML programmaticaly on my securityTokenHandlers collection?
<microsoft.identityModel>
<service>
<audienceUris>
<add value="http://www.someapp.net/" />
</audienceUris>
<federatedAuthentication>
<wsFederation passiveRedirectEnabled="true" issuer="https://rd-test.accesscontrol.appfabriclabs.com/v2/wsfederation" realm="http://www.thisapp.net" requireHttps="false" />
<cookieHandler requireSsl="false" />
</federatedAuthentication>
<applicationService>
<claimTypeRequired>
<claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true" />
<claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true" />
</claimTypeRequired>
</applicationService>
<issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<trustedIssuers>
<add thumbprint="XYZ123" name="https://somenamespace.accesscontrol.appfabriclabs.com/" />
</trustedIssuers>
</issuerNameRegistry>
</service>
I was struggling with the same and found a working solution in WIF 3.5/4.0. Since maartenba's link seems to be dead, I wanted to post my solution here.
Our requirements were:
Configuration fully in code (as we ship a default web.config with the app)
Maximum allowed .Net version 4.0 (hence I am using WIF 3.5/4.0)
What I used to arrive at the solution:
Information about dynamic WIF configuration provided by Daniel Wu
here.
This
method
to register HTTP modules at runtime, explained by David Ebbo. I
also tried the more elegant method explained by Rick
Strahl,
but that unfortunately did not do the trick for me.
Edit 2016/09/02: instead of adding a separate "pre application start
code" class as in David Ebbo's example, the WIF-related HTTP modules
can also be registered in the static constructor of the
`HttpApplication' class. I have adapted the code to this somewhat
cleaner solution.
My solution needs nothing in web.config. The bulk of the code is in global.asax.cs. Configuration is hard-coded in this sample:
using System;
using System.IdentityModel.Selectors;
using System.Security.Cryptography.X509Certificates;
using System.Web;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Web;
namespace TestADFS
{
public class SessionAuthenticationModule : Microsoft.IdentityModel.Web.SessionAuthenticationModule
{
protected override void InitializePropertiesFromConfiguration(string serviceName)
{
}
}
public class WSFederationAuthenticationModule : Microsoft.IdentityModel.Web.WSFederationAuthenticationModule
{
protected override void InitializePropertiesFromConfiguration(string serviceName)
{
ServiceConfiguration = FederatedAuthentication.ServiceConfiguration;
PassiveRedirectEnabled = true;
RequireHttps = true;
Issuer = "https://nl-joinadfstest.joinadfstest.local/adfs/ls/";
Realm = "https://67px95j.decos.com/testadfs";
}
}
public class Global : HttpApplication
{
static Global()
{
Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(SessionAuthenticationModule));
Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(WSFederationAuthenticationModule));
}
protected void Application_Start(object sender, EventArgs e)
{
FederatedAuthentication.ServiceConfigurationCreated += FederatedAuthentication_ServiceConfigurationCreated;
}
internal void FederatedAuthentication_ServiceConfigurationCreated(object sender, Microsoft.IdentityModel.Web.Configuration.ServiceConfigurationCreatedEventArgs e)
{
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection coll = store.Certificates.Find(X509FindType.FindByThumbprint, "245537E9BB2C086D3C880982FA86267FBD66B9A3", false);
if (coll.Count > 0)
e.ServiceConfiguration.ServiceCertificate = coll[0];
store.Close();
AudienceRestriction ar = new AudienceRestriction(AudienceUriMode.Always);
ar.AllowedAudienceUris.Add(new Uri("https://67px95j.decos.com/testadfs"));
e.ServiceConfiguration.AudienceRestriction = ar;
ConfigurationBasedIssuerNameRegistry inr = new ConfigurationBasedIssuerNameRegistry();
inr.AddTrustedIssuer("6C9B96D90257B65B6F181C2478D869473DC359EA", "http://NL-JOINADFSTEST.joinadfstest.local/adfs/services/trust");
e.ServiceConfiguration.IssuerNameRegistry = inr;
e.ServiceConfiguration.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.None;
}
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
FederatedAuthentication.WSFederationAuthenticationModule.ServiceConfiguration = FederatedAuthentication.ServiceConfiguration;
}
}
}
Usage
My app is asp.net WebForms, running in classic pipeline mode and supports forms authentication as well as ADFS login. Because of that, authentication is handled in a common base class shared by all .aspx pages:
protected override void OnInit(EventArgs e)
{
if (NeedsAuthentication && !User.Identity.IsAuthenticated)
{
SignInRequestMessage sirm = new SignInRequestMessage(
new Uri("https://nl-joinadfstest.joinadfstest.local/adfs/ls/"),
ApplicationRootUrl)
{
Context = ApplicationRootUrl,
HomeRealm = ApplicationRootUrl
};
Response.Redirect(sirm.WriteQueryString());
}
base.OnInit(e);
}
In this code, ApplicationRootUrl is the application path ending in "/" (the "/" is important in Classic pipeline mode).
As a stable implementation for logout in mixed mode was not so easy, I want to show the code for that as well. Technically it works, but I still have an issue with IE immediately logging in again after logging out an ADFS account:
if (User.Identity.IsAuthenticated)
{
if (User.Identity.AuthenticationType == "Forms")
{
FormsAuthentication.SignOut();
Session.Clear();
Session.Abandon();
ResetCookie(FormsAuthentication.FormsCookieName);
ResetCookie("ASP.NET_SessionId");
Response.Redirect(ApplicationRootUrl + "Default.aspx");
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
else
{
FederatedAuthentication.SessionAuthenticationModule.SignOut();
FederatedAuthentication.SessionAuthenticationModule.DeleteSessionTokenCookie();
Uri uri = new Uri(ApplicationRootUrl + "Default.aspx");
WSFederationAuthenticationModule.FederatedSignOut(
new Uri("https://nl-joinadfstest.joinadfstest.local/adfs/ls/"),
uri); // 1st url is single logout service binding from adfs metadata
}
}
(ResetCookie is a helper function that clears a response cookie and sets its expiration in the past)
Just a thought, no idea whether this works: Isn't there a way to get at the actual XML (which is empty in your case) and modify it at runtime through the classes in Microsoft.IdentityModel.Configuration?
Alternatively, some of the things in the XML you can modify at the time the sign-in request is sent out, in the RedirectingToIdentityProvider event by modifying the SignInRequestMessage
FYI: found a solution and implemented it in a module described (and linked) here: http://blog.maartenballiauw.be/post/2011/02/14/Authenticate-Orchard-users-with-AppFabric-Access-Control-Service.aspx

Resources