Patching Asp.net Mvc2 AntiForgeryToken exception - asp.net-mvc

Some background to my issue:
It appears that there is a change/bug in Mvc2 concerning ValidateAntiForgeryTokenAttribute.
When upgrading from Mvc1 to Mvc2, users with an active session will receive the following error when they request a page using ValidateAntiForgeryTokenAttribute:
Unable to cast object of type 'System.Web.UI.Triplet' to type 'System.Object[]'.
The issue is documented here.
After upgrading to Mvc2, we anticipate that we will be seriously affected by this issue. I have coded a fix derived from the code in the comments (documented below for posterity). At the moment this code is invoked by creating a subclass of Controller and AsyncController overriding the Initialize method in order to correct the problem. e.g.
public class FixedController:Controller
{
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
this.FixAntiForgeryTokenMvc1ToMvc2(requestContext); //extension
}
}
internal static class ControllerEx
{
public static void FixAntiForgeryTokenMvc1ToMvc2(
this Controller controller,
RequestContext requestContext)
{
var cc = new ControllerContext(requestContext,
controller);
var antiForgeryAttribute = new ValidateAntiForgeryTokenAttribute();
try
{
antiForgeryAttribute.OnAuthorization(new AuthorizationContext(cc));
}
catch (HttpAntiForgeryException forgeryException)
{
var castException = forgeryException.InnerException;
if (castException != null
&& castException is InvalidCastException
&& castException.Message.StartsWith(
"Unable to cast object of type"
+ " 'System.Web.UI.Triplet' to type"
+ " 'System.Object[]'"))
{
var responseTokenCookieNames =
controller
.Response
.Cookies
.Cast<Cookie>()
.Select(c => c.Name)
.Where(n => n.Contains("RequestVerificationToken"));
foreach (var s in responseTokenCookieNames)
{
var cookie = controller.Response.Cookies[s];
if (cookie != null)
{
cookie.Value = "";
}
}
var requestTokenCookieNames =
controller
.Request
.Cookies
.Cast<String>()
.Where(n => n.Contains("RequestVerificationToken"))
.ToList();
foreach (var c in requestTokenCookieNames)
{
controller.Request.Cookies.Remove(c);
}
}
}
}
}
The knock on effect of this is that I must alter all my controller classes to derive from my new, corrected Controller subclasses. This seems quite intrusive for code that intend to deprecate in about a month's time.
So, coming to my question, I'd like to know if there is a less intrusive means of patching existing classes such that downstream users of the class do not have to be altered, perhaps using reflection?

Spender,
rather than code every controller, use a basecontroller and inherit from that along the lines of:
public abstract class BaseController : Controller
{
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
FixAntiForgeryTokenMvc1ToMvc2(this, requestContext);
}
private static void FixAntiForgeryTokenMvc1ToMvc2(
Controller controller, RequestContext requestContext)
{
// stuff ....
}
}
and in your 'normal' controllers, just have:
public class NormalController : BaseController
{
// all the previous stuff
}
give it a go...

Related

Extend OutputCacheAttribute in MVC 3

What caching mechanism does OutputCacheAttribute use and how can i extend it?
It uses ASP.NET WebForm caching, and you can extend it by overriding OnResultExecuting.
Refer to:
http://forums.asp.net/t/1703571.aspx/1?Extending+OutputCache+Attribute+in+asp+net+mvc+3
Code extract from above link.
public override void OnResultExecuting(ResultExecutingContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (!filterContext.IsChildAction) {
// we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) {
page.ProcessRequest(HttpContext.Current);
}
}
}
private sealed class OutputCachedPage : Page {
private OutputCacheParameters _cacheSettings;
public OutputCachedPage(OutputCacheParameters cacheSettings) {
// Tracing requires Page IDs to be unique.
ID = Guid.NewGuid().ToString();
_cacheSettings = cacheSettings;
}
protected override void FrameworkInitialize() {
// when you put the <%# OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
base.FrameworkInitialize();
InitOutputCache(_cacheSettings);
}
}

Test if Attribute is on Controller or Action

When an Attribute fires, can I test if was set on a Controller or an Action?
Behavior I want is: use Action attribute if exists, else use Controller attribute. Something like this:
public class TestAttribute : FilterAttribute, IAuthorizationFilter
{
public TestAttribute(string optionalParam = "") { /*...*/ }
public void OnAuthorization(AuthorizationContext filterContext)
{
bool isClassAttribute; // = ????
bool hasActionAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof(TestAttribute ), false).Length > 0;
if (isClassAttribute && hasActionAttribute)
return; // handle in Action attribute
else
; // do stuff with optionalParam...
}
}
[TestAttribute]
public class TestClass
{
[TestAttribute(optionalParam:"foo"]
public ActionResult TestMethod() { return null; }
}
I could do this with the Order property, but don't want to have to set it every time (or get funky).
Edit / Solution
OK, found a solution to my issue (but not the question) - setting the Attribute base parameter AllowMultiple=false means last instance of the same filter type is allowed, and all others are discarded (and Controller attributes run first(?) so should be good to go...).
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class TestAttribute : FilterAttribute, IAuthorizationFilter
{
public TestAttribute(string optionalParam = "") { /*...*/ }
public void OnAuthorization(AuthorizationContext filterContext)
{
// this should be the Action attribute (if exists), else the Controller attribute...
}
}
Anyway I asked a slightly different question, so will still give points for answer ;)
I believe the item executes on each method call anyway but you can reference for instance:
public void OnActionExecuting(ActionExecutingContext filterContext) { }
..
..
string controllerName = filterContext.Controller.GetType().Name;
//either or:
string actionMethodName = filterContext.ActionDescriptor.ActionName;
string actionMethodName = filterContext.RouteData.Values["action"].ToString();
If your actionMethodName is null, then it's potentially from your controller - although like I said they may only get called when an action method is called as it is (not 100% sure on this one though test out the above code and that should answer your question)
Hope it helps : )

ControllerFactory for specific portable area

My main ASP.NET MVC composite application uses a global Unity container to register types. The app sets the controller factory up to use this global container. I would like to refactor this such that each one of my portable areas leverages it's own child Unity container, so that different areas can implement interfaces in varying ways. It seems like I would need to have a different ControllerFactory per area. How would I accomplish that, given that the following sets the factory for all?
ControllerBuilder.Current
.SetControllerFactory(/* controller factory with global unity container */);
You can use a MasterControllerFactory that contains all the IControllerFactory implementations for each area, and knows which factory can build which RequestContext. This actually allows you to select a different ControllerFactory for any variation, not just by area. Here's how it works:
All of the area controller factory implementations must implement IFilteredControllerFactory instead of IControllerFactory. Here it is:
public interface IFilteredControllerFactory:IControllerFactory
{
bool CanHandle(RequestContext requestContext);
}
An example of an implementation that filters based on the area name looks like this:
public class Area51ControllerFactory:IFilteredControllerFactory
{
public bool CanHandle(RequestContext requestContext)
{
return requestContext.RouteData.DataTokens["area"].ToString().ToLowerInvariant() == "area51";
}
public IController CreateController(RequestContext requestContext, string controllerName)
{
// create a controller...
}
public void ReleaseController(IController controller)
{
// release the controller...
}
}
Then you need the MasterControllerFactory, which looks like this:
public class MasterControllerFactory : DefaultControllerFactory
{
private readonly List<IFilteredControllerFactory> _slaveFactories;
public MasterControllerFactory()
{
_slaveFactories = new List<IFilteredControllerFactory>();
}
public void RegisterFactory(IFilteredControllerFactory slaveFactory)
{
if(slaveFactory!=null && !_slaveFactories.Contains(slaveFactory))
{
_slaveFactories.Add(slaveFactory);
}
}
public override IController CreateController(RequestContext requestContext, string controllerName)
{
var factory = _slaveFactories.FirstOrDefault(x => x.CanHandle(requestContext));
if(factory!=null)
{
return factory.CreateController(requestContext, controllerName);
}
return base.CreateController(requestContext, controllerName);
}
public override void ReleaseController(IController controller)
{
bool released = false;
if (controller is Controller)
{
var requestContext = ((Controller) controller).ControllerContext.RequestContext;
var factory = _slaveFactories.FirstOrDefault(x => x.CanHandle(requestContext));
if (factory != null)
{
factory.ReleaseController(controller);
released = true;
}
}
if(!released)base.ReleaseController(controller);
}
}
In the Application_Start of your global.asax you still need to set everything up, but that's easy.
var masterControllerFactory = new MasterControllerFactory();
masterControllerFactory.Register(new Area51ControllerFactory());
ControllerBuilder.Current.SetControllerFactory(masterControllerFactory);
Obviously, you can tweak this a number of ways to work best with your coding style and application architecture.
I don't think you can have different factory per area, but you can in your single factory, put the controller into different container, based on the area. I think but not sure, that you can find your area in CreateController method of your factory by querying this:
requestContext.RouteData.DataTokens["area"];
where requestContext is passed to CreateController as a parameter.

TempData are always empty

I want to use TempData to store messages between Post and followed redirect but TempData are always empty.
I have BaseContoller offering some infrastructure for passing TempData. Simplified code looks like:
public abstract class BaseController : Controller
{
public const string AuditMessagesKey = "AuditMessages";
private List<InformationMessage> _informationMessages = new List<InformationMessage>();
protected BaseController()
{
// I also tried this in overriden Initialize
ViewData[AuditMessagesKey] = GetAuditMessages();
}
protected void AddAuditMessage(InformationMessage message)
{
if (message == null)
return;
_informationMessages.Add(message);
}
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
if (filterContext.Result is RedirectToRouteResult)
{
// I see that messages are stored into TempData
TempData[AuditMessagesKey] = _informationMessages;
// This also doesn't help
// TempData.Keep(AuditMessagesKey);
}
}
private ICollection<InformationMessage> GetAuditMessages()
{
// TempData are always empty here
var messages = TempData[AuditMessagesKey] as List<InformationMessage>;
if (messages == null)
{
messages = new List<InformationMessage>();
}
return messages;
}
}
Action method looks like:
[HttpPost]
public ActionResult CancelEdit(RequestSaveModel model)
{
AddAuditMessage(new InformationMessage
{
Message = String.Format(Messages.RequestEditationCanceled, model.Title),
Severity = MessageSeverity.Information
});
return RedirectToAction("Detail", new { Id = model.Id});
}
Application is tested on VS Development web server. There are no Ajax calls and I removed all Html.RenderAction calls from my master page. I can see that TempData are accessed only once per request in GetAuditedMessages and stored only once in OnResultExecuting. Nothing overwrites the data. Session state is allowed.
The code is little bit simplified. We are also using antiforgery token, custom filters for authorization and for action selection but it should not affect TempData behavior.
I don't understand it. I used TempData before in test application and it worked fine.
The problem I see in your code is that you are trying to retrieve the data from TempData in the controller's constructor - which is before it is available.
Move the call to GetAuditMessages() into an OnActionExecuting method, and it will be accessible.
public abstract class BaseController : Controller
{
public const string AuditMessagesKey = "AuditMessages";
private List<InformationMessage> _informationMessages = new List<InformationMessage>();
protected BaseController()
{
// TempData is not available yet
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
ViewData[AuditMessagesKey] = GetAuditMessages();
base.OnActionExecuting(filterContext);
}
protected void AddAuditMessage(InformationMessage message)
{
if (message == null)
return;
_informationMessages.Add(message);
}
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
if (filterContext.Result is RedirectToRouteResult)
{
// I see that messages are stored into TempData
TempData[AuditMessagesKey] = _informationMessages;
// This also doesn't help
// TempData.Keep(AuditMessagesKey);
}
}
private ICollection<InformationMessage> GetAuditMessages()
{
var messages = TempData[AuditMessagesKey] as List<InformationMessage>;
if (messages == null)
{
messages = new List<InformationMessage>();
}
return messages;
}
}
I think this is what's happening:
In CancelEdit, your RedirectToAction is returned, and the framework redirects to "Detail". In your Detail method, the ActionExecuting fires, but it's filterContext.Result is not your RedirectToAction result - it's a new result (actually, no result as of yet).
Do you need the check for "filterContext.Result is RedirectToRouteResult"? It seems that you will only have those messages added before you perform a redirect.
in my Solution I forgot to remove HttpCookies for my development
it just work on published site in Https
<httpCookies httpOnlyCookies="true" requireSSL="true" />

ASP.NET MVC application controller constructor problem

I've created an application controller abstract class that my controllers derive from (As described in the following article)
The following is an example of what my code looks like
public abstract class ApplicationController : Controller
{
private ProjectDataContext datacontext = new ProjectDataContext();
protected ProjectDataContext DataContext
{
get { return datacontext; }
}
public ApplicationController()
{
ViewData["OpenTasks"] = DataContext.Tasks.Where(t => t.UserId == this.UserId).Count();
}
}
This produces the following error which i have determined is due to the "Where" lamda expression:
If the controller doesn't have a controller factory, ensure that it has a parameterless public constructor.
this error is produced whichever way i write the LINQ query and the only way to compile the application is by removing the "Where" clause as follows.
ViewData["OpenTasks"] = DataContext.Tasks.Count();
any ideas what the problem is or how to resolve this as i need to execute the query against the user and not return all entries.
thanks in advance
Try this instead of using the constructor:-
public abstract class ApplicationController : Controller
{
private ProjectDataContext datacontext = new ProjectDataContext();
protected ProjectDataContext DataContext
{
get { return datacontext; }
}
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
base.Initialize(RequestContext);
ViewData["OpenTasks"] = DataContext.Tasks.Where(t => t.UserId == this.UserId).Count();
}
}
Its quite likely that the current user ID is dependand on the RequestContext
It could be that the call is crashing in the constructor because the UserId (this.UserId) hasn't been initialised yet.

Resources