If I receive a request from a Spider, I kick off a Phantom JS process and render back dynamic HTML. (Using a OnExecuting filter and setting the ActionResult)
But the OutputCache filter is in place on this method as well and it is getting in the way!.
E.G:
step 1. Load page with normal user agent. (Output cache caches the URL)
step 2. Load page with spider user agent. (the previous cached response is sent to the spider, and my Phantom JS filter never runs)
Use VaryByCustom to force a 'Cache Miss' when the request is from a Search Engine Crawler.
In your Controller/Action:
[OutputCache(VaryByCustom="Crawler")]
public ActionResult Index()
{
// ...
return View();
}
Then in your Global.asax:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "Crawler" && context.Request.Browser.Crawler)
return Guid.NewGuid().ToString();
return base.GetVaryByCustomString(context, arg);
}
Related
We have an ASP.NET MVC web application where the user is redirected to the original url after login if the user had tried to reach a certain url while not being authenticated.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel viewModel, string returnUrl = null) //relative url
{
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
The problem is that some urls contain encoded characters, e.g. /bla/bla?param=abc%23. They are decoded by .NET (correct?) so that the url looks like /bla/bla?param=abc# on the server. This causes the redirect to stop working, as param is interpreted as 'abc' in the target action.
What is best practice to handle this problem? I tried WebUtility.UrlEncode(returnUrl), but it seems that the routing engine cannot handle the resulting url.
Please correct anytime if my reasoning is wrong.
I need an advice. I wrote an MVC app to access Google Gmail API (using Google OAuth 2 Authentication), as explained in Google's Tutorial: https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-aspnet-mvc
The idea is - Index action initializes
private static AuthorizationCodeWebApp.AuthResult result;
after it performs Google OAuth2 authentication. That result object has Credentials.Token property that contains issued AccessToken and RefreshToken. Next, Index action returns a vew (rendered HTML) that has a DIV where I want to load Gmail messages asynchronously. When the page loads, there is a Javascript function that fires on page load even in a browser. Simple. That Javascript function makes an AJAX call to Gmail() action in HomeController. It makes these calls with a 1 minute interval (js timer). Since result object is marked as static it should be available to pass Credentials to Gmail API Service method that is implemented in GmailManager helper class:
return PartialView(GmailManager.GetGmail(result.Credential));
The problem is sometimes the result object is null inside the Gmail() action and the NullReferenceException is thrown
I dont understand why this is happening if the result was initialized in Index action and it is static, so it should be alive by the time the call is made to Gmail() action. It should never be null. If it was constantly null I would understand and try to debug and fix it, but it is random and I cannot understand the logic.
If someone understands what is happening please advice.
Below is my HomeController code:
public class HomeController : Controller
{
private static AuthorizationCodeWebApp.AuthResult result;
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential == null)
return new RedirectResult(result.RedirectUri);
if (!string.IsNullOrEmpty(result.Credential.Token.RefreshToken))
SaveRefreshTocken(result.Credential.Token.RefreshToken);
return View();
}
public ActionResult Gmail()
{
result.Credential.Token.RefreshToken = WebConfigurationManager.AppSettings["RefreshToken"];
return PartialView(GmailManager.GetGmail(result.Credential));
}
private static void SaveRefreshTocken(string refreshToken)
{
Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
config.AppSettings.Settings["RefreshToken"].Value = refreshToken;
config.Save();
}
}
I am trying to provide a progress monitoring mechanism for a longish-running request implemented by an AsyncController. A web page invokes (via JQuery $.post) the asynchronous StartTask action on the following controller...
[NoAsyncTimeout]
public class TaskController: AsyncController
{
[HttpPost]
public void StartTaskAsync()
{
AsyncManager.OutstandingOperations.Increment();
Session["progress"] = "in progress";
Task.Factory.StartNew(DoIt);
}
public ActionResult StartTaskCompleted()
{
return Json(new {redirectUrl = Url.Action("TaskComplete", "First")});
}
private void DoIt()
{
try
{
// Long-running stuff takes place here, including updating
// Session["progress"].
}
finally
{
AsyncManager.OutstandingOperations.Decrement();
}
}
}
The same web page sets up a 2-second timer (via window.setInterval) to call the following controller (via JQuery $.getJSON) that has read-only access to the session...
[SessionState(SessionStateBehavior.ReadOnly)]
public class ProgressController: Controller
{
[HttpGet]
public ActionResult Current()
{
var data = Session["progress"];
return Json(data, JsonRequestBehavior.AllowGet);
}
}
The function invoked after getJSON returns updates a DIV to show the current progress. Ultimately the progress will be updated in the session by the 'long-running stuff takes place here' code, but as it is the call to ProgressController.Current() does not find any data in the session and always returns null.
I had assumed that JQuery AJAX requests sent from the same browser to the same IIS server would end up with the same session, but it seems not. Do I need to explicitly set the ASP.NET session key on these $.post and $.getJSON calls? Or is it just not possible to share session state between Controllers (even if one has R/O access) in this way (I can fall back to a slightly hacky solution with Application state and GUIDs).
That's normal behavior. Your StartTask action blocks until it completes. It doesn't return any response until the DoIt method has finished executing. I suppose that's why you are calling it with AJAX $.post. The problem is that while this method runs and writes to the session and all other requests from the same session will be queued by the ASP.NET engine. You cannot write and read from the session at the same time. You will have to find another thread-safe storage for the progress data other than the session.
I have a portion of my site that has a lightweight xml/json REST API. Most of my site is behind forms auth but only some of my API actions require authentication.
I have a custom AuthorizeAttribute for my API that I use to check for certain permissions and when it fails it results in a 401. All is good, except since I'm using forms auth, Asp.net conveniently converts that into a 302 redirect to my login page.
I've seen some previous questions that seem a bit hackish to either return a 403 instead or to put some logic in the global.asax protected void Application_EndRequest()
that will essentially convert 302 to 401 where it meets whatever criteria.
Previous Question
Previous Question 2
What I'm doing now is sort of like one of the questions, but instead of checking the Application_EndRequest() for a 302 I make my authorize attribute return 666 which indicates to me that I need to set this to a 401.
Here is my code:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == MyAuthAttribute.AUTHORIZATION_FAILED_STATUS)
{
//check for 666 - status code of hidden 401
Context.Response.StatusCode = 401;
}
}
Even though this works, my question is there something in Asp.net MVC 2 that would prevent me from having to do this? Or, in general is there a better way? I would think this would come up a lot for anyone doing REST api's or just people that do ajax requests in their controllers. The last thing you want is to do a request and get the content of a login page instead of json.
How about decorating your controller/actions with a custom filter:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class RequiresAuthenticationAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
var user = filterContext.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
}
}
and in your controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[RequiresAuthentication]
public ActionResult AuthenticatedIndex()
{
return View();
}
}
Another way of doing this is to implement a custom ActionResult. In my case, I wanted one anyway, since I wanted a simple way of sending data with custom headers and response codes (for a REST API.) I found the idea of doing a DelegatingActionResult and simply added to it a call to Response.End(). Here's the result:
public class DelegatingActionResult : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
Command(context);
// prevent ASP.Net from hijacking our headers
context.HttpContext.Response.End();
}
private readonly Action<ControllerContext> Command;
public DelegatingActionResult(Action<ControllerContext> command)
{
if (command == null)
throw new ArgumentNullException("command");
Command = command;
}
}
The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.
Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:
$(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
// if request returns non MVC page reload because this means the user
// session has expired
var mvcHeaderName = "X-AspNetMvc-Version";
var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);
if (!mvcHeaderValue) {
location.reload();
}
});
The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.
If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.
TurnOffTheRedirectionAtIIS
From MSDN, This article explains how to avoid the redirection of 401 responses : ).
Citing:
Using the IIS Manager, right-click the
WinLogin.aspx file, click Properties,
and then go to the Custom Errors tab
to Edit the various 401 errors and
assign a custom redirection.
Unfortunately, this redirection must
be a static fileāit will not process
an ASP.NET page. My solution is to
redirect to a static Redirect401.htm
file, with the full physical path,
which contains javascript, or a
meta-tag, to redirect to the real
ASP.NET logon form, named
WebLogin.aspx. Note that you lose the
original ReturnUrl in these
redirections, since the IIS error
redirection required a static html
file with nothing dynamic, so you will
have to handle this later.
Hope it helps you.
I'm still using the end request technique, so I thought I would make that the answer, but really
either of the options listed here are generally what I would say are the best answers so far.
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == MyAuthAttribute.AUTHORIZATION_FAILED_STATUS)
{
//check for 666 - status code of hidden 401
Context.Response.StatusCode = 401;
}
}
By default, ASP.NET's membership provider redirects to a loginUrl when a user is not authorized to access a protected page.
Is there a way to display a custom 403 error page without redirecting the user?
I'd like to avoid sending users to the login page and having the ReturnUrl query string in the address bar.
I'm using MVC (and the Authorize attribute) if anyone has any MVC-specific advice.
Thanks!
I ended up just creating a custom Authorize class that returns my Forbidden view.
It works perfectly.
public class ForbiddenAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext))
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else
{
// auth failed, display 403 page
filterContext.HttpContext.Response.StatusCode = 403;
ViewResult forbiddenView = new ViewResult();
forbiddenView.ViewName = "Forbidden";
filterContext.Result = forbiddenView;
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
}
Asp.net has had what I consider a bug in the formsauth handling of unauthenticated vs underauthenticated requests since 2.0.
After hacking around like everyone else for years I finally got fed up and fixed it. You may be able to use it out of the box but if not I am certain that with minor mods it will suit your needs.
be sure to report success or failure if you do decide to use it and I will update the article.
http://www.codeproject.com/Articles/39062/Salient-Web-Security-AccessControlModule.aspx