MVC3 Redirect Loop - asp.net-mvc

I have been searching for a solution to this issue for over 10 hours with no answer. In my application I am using the [requirehttps] attribute. When clicking an action method decorated with this attribute I get “cannot display the webpage" in IE. After digging into that issue I saw that I was receiving infinite 302 calls in Fiddler, which would eventually timeout and cause that error. So I decided to create a custom attribute and physically create the https call. I am using IIS Express and successfully created a certificate and binded the port. If I call this URL directly through the browser everything works fine. This is the code I have been using to redirect the request.
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext){
var builder = new UriBuilder(HttpContext.Current.Request.Url);
if (RequireSecure){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
}
protected virtual void HandleNonHttpRequest(AuthorizationContext filterContext){
if (String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)){
// redirect to HTTP version of page
string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
}
When I set a breakpoint to find out what the builder value is, it is correct. And it is from here I receive an infinite redirect loop. The strange thing is that the first request is never correct when I look at the URL within the browser. It is as if UriBuilder is not sending the correct URL. Any ideas? This is driving me nuts.

The redirect loop is occurring because the code is only checking if the HTTPS redirect is required, not if the current request is already HTTPS (i.e. the redirect has already happened).
if (RequireSecure && !filterContext.HttpContext.Request.IsSecureConnection){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
Although the RequireHttps should work correctly for this, unless you need to redirect to the port specified.
EDIT:
Refactored attribute
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
var requestUri = HttpContext.Current.Request.Url;
var requestIsSecure = HttpContext.Current.Request.IsSecureConnection;
if (RequireSecure && !requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttps, 44300);
else if (!RequireSecure && requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttp, 80);
}
private RedirectResult Redirect(Uri uri, string scheme, int port)
{
return new RedirectResult(new UriBuilder(uri) { Scheme = scheme, Port = port }.Uri.ToString());
}
}

Related

trapping an exception using IExceptionFilter and redirecting permanently binds the path to error page

I am trying to redirect unhandled exceptions to an error page by first generating a crash code and then redirecting the user to the error page. The problem is that after the first time it happens, the original url/action is permanently mapped to the error page url and doesn't even enter the OnException method anymore. Even after I fix the cause of the original exception, the url/action is still mapped to just redirect to the error page. Not sure where this is happening or how to fix it. Below is the code:
public class UnhandledExceptionFilter : IExceptionFilter
{
protected static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public void OnException(ExceptionContext filterContext)
{
var ex = filterContext.Exception;
if (ex != null)
{
string user = "";
try
{
user = Managers.UserManager.FindByUsername(filterContext.HttpContext.User.Identity.Name, true).FullName;
}
catch
{
}
var url = filterContext.HttpContext.Request.Url.ToString();
var urlRef = filterContext.HttpContext.Request.UrlReferrer.PathAndQuery;
var randStr = StringExtensions.GenerateRandomAlphaNumerics(new System.Random(), 4);
Logger.Error(ex, "crashCode={0} - user={1} - url={2} - urlRef={3}", randStr, user, url, urlRef);
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.ExceptionHandled = true;
filterContext.Result = new RedirectResult("/misc/whoops?crash_code=" + randStr, true);
}
}
}
The problem should be solved by not passing true to the second parameter of RedirectResult. Pass false instead:
filterContext.Result = new RedirectResult("/misc/whoops?crash_code=" + randStr, false);
Passing true as the second parameter indicates the redirect is permanent. Which is, based on your question, exactly want you do not want.
See: MSDN RedirectResult and
301 vs 302 redirects

Programatically authenticate AzureAd/OpenId to an MVC controller using C# and get redirect uri

I have overridden the built in WebClient as below. Then I call it
public class HttpWebClient : WebClient
{
private Uri _responseUri;
public Uri ResponseUri
{
get { return _responseUri; }
}
protected override WebResponse GetWebResponse(WebRequest request)
{
WebResponse response = base.GetWebResponse(request);
_responseUri = response.ResponseUri;
return response;
}
}
Then I consume it like this:
using (HttpWebClient client = new HttpWebClient())
{
client.Headers[HttpRequestHeader.Authorization] = $"Bearer { _token }";
client.Headers[HttpRequestHeader.ContentType] = "application/json";
client.UploadData(_url, Encoding.UTF8.GetBytes(_data));
string queryString = client.ResponseUri.Query.Split('=').Last();
}
The response uri comes back with "https://login.microsoftonline" rather than url returned from the MVC controller with a query string, as it is authenticating first with that bearer token using AzureAd/OpenId. If i call it twice it returns the original _url but not the redirected one. If I remove AzureAd authentication it works fine. Is there a way to force the response uri to come back as what the MVC controller sets it to?
Assuming you use the 'UseOpenIdConnectAuthentication' and configuring it to use AAD authentication, you can modify the redirect uri by setting Notifications.RedirectToIdentityProvider, something like:
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async _ =>
{
_.ProtocolMessage.RedirectUri = _.Request.Uri.ToString();
}
}
If you use something else , or maybe I didn't understand your problem - please supply more information

Once a user visits a [RequireHTTPS] action, they stay on https

I'm securing some of my ActionResults with [RequireHttps] and that's working great.
People are keeping their https connection after they go to other links. I only want https on certain pages that I specify, I want http on everything else.
You could try creating a base controller and override the OnActionExecuting method to do something like:
protected override void OnActionExecuting(ActionExecutingContext ctx) {
{
bool redirect = true;
if (!ctx.HttpContext.Request.IsSecureConnection) redirect = false;
// Bypass if [RequireHttps] is applied
if (ctx.ActionDescriptor.ControllerDescriptor.GetCustomAttributes
(typeof(RequireHttpsAttribute), true).Length > 0) redirect = false;
if (ctx.ActionDescriptor.GetCustomAttributes
(typeof(RequireHttpsAttribute), true).Length > 0) redirect = false;
if (!redirect)
{
base.OnActionExecuting(ctx);
}
else
{
// Redirect to HTTP
string url = "http://" + ctx.HttpContext.Request.Url.Host
+ ctx.HttpContext.Request.RawUrl;
ctx.Result = new RedirectResult(url);
}
}

Create Custom 301 Redirect Page

I am attempting to write a 301 redirect scheme using a custom route (class that derives from RouteBase) similar to Handling legacy url's for any level in MVC. I was peeking into the HttpResponse class at the RedirectPermanent() method using reflector and noticed that the code both sets the status and outputs a simple HTML page.
this.StatusCode = permanent ? 0x12d : 0x12e;
this.RedirectLocation = url;
if (UriUtil.IsSafeScheme(url))
{
url = HttpUtility.HtmlAttributeEncode(url);
}
else
{
url = HttpUtility.HtmlAttributeEncode(HttpUtility.UrlEncode(url));
}
this.Write("<html><head><title>Object moved</title></head><body>\r\n");
this.Write("<h2>Object moved to here.</h2>\r\n");
this.Write("</body></html>\r\n");
This is desirable, as in my experience not all browsers are configured to follow 301 redirects (although the search engines do). So it makes sense to also give the user a link to the page in case the browser doesn't go there automatically.
What I would like to do is take this to the next level - I want to output the result of an MVC view (along with its themed layout page) instead of having hard coded ugly generic HTML in the response. Something like:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here
response.Write(The View)
response.End();
}
How can I write the output of a view to the response stream?
Additional Information
In the past, we have had problems with 301 redirects from mydomain.com to www.mydomain.com, and subsequently got lots of reports from users that the SSL certificate was invalid. The search engines did their job, but the users had problems until I switched to a 302 redirect. I was actually unable to reproduce it, but we got a significant number of reports so something had to be done.
I plan to make the view do a meta redirect as well as a javascript redirect to help improve reliability, but for those users who still end up at the 301 page I want them to feel at home. We already have custom 404 and 500 pages, why not a custom themed 301 page as well?
It turns out I was just over-thinking the problem and the solution was much simpler than I had envisioned. All I really needed to do was use the routeData to push the request to another controller action. What threw me off was the extra 301 status information that needed to be attached to the request.
private RouteData RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.CacheControl = "no-cache";
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Redirect301";
routeData.Values["url"] = destinationUrl;
return routeData;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Do stuff here...
if (this.IsDefaultUICulture(cultureName))
{
var urlWithoutCulture = url.ToString().ToLowerInvariant().Replace("/" + cultureName.ToLowerInvariant(), "");
return this.RedirectPermanent(urlWithoutCulture, httpContext);
}
// Get the page info here...
if (page != null)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = page.ContentType.ToString();
result.Values["action"] = "Index";
result.Values["id"] = page.ContentId;
}
return result;
}
I simply needed to return the RouteData so it could be processed by the router!
Note also I added a CacheControl header to prevent IE9 and Firefox from caching the redirect in a way that cannot be cleared.
Now I have a nice page that displays a link and message to the user when the browser is unable to follow the 301, and I will add a meta redirect and javascript redirect to the view to boot - odds are the browser will follow one of them.
Also see this answer for a more comprehensive solution.
Assuming you have access to the HttpContextBase here's how you could render the contents of a controller action to the response:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here (HomeController/SomeAction in this example)
var routeData = new RouteData();
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "SomeAction";
IController homeController = new HomeController();
var rc = new RequestContext(new HttpContextWrapper(context), routeData);
homeController.Execute(rc);
response.End();
}
This will render SomeAction on HomeController to the response.

More control on ASP.Net MVC's Authorize; to keep AJAX requests AJAXy

I have some action methods behind an Authorize like:
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(int siteId, Comment comment) {
The problem I have is that I'm sending a request through AJAX to Comment/Create with
X-Requested-With=XMLHttpRequest
which helps identify the request as AJAX. When the user is not logged in and hits the Authorize wall it gets redirected to
/Account/LogOn?ReturnUrl=Comment%2fCreate
which breaks the AJAX workflow. I need to be redirected to
/Account/LogOn?X-Requested-With=XMLHttpRequest
Any ideas how that can be achieved? Any ways to gain more control over what happens when Authorization is requested?
Thanks to Lewis comments I was able to reach this solution (which is far from perfect, posted with my own comments, if you have the fixes feel free to edit and remove this phrase), but it works:
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
override public void OnAuthorization(AuthorizationContext filterContext) {
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest()) {
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
// 2- When leaving Result as a HttpUnauthorizedResult, ASP.Net actually does some nice automatic stuff, like adding a ReturnURL, when hardcodding the URL here, that is lost.
String url = System.Web.Security.FormsAuthentication.LoginUrl + "?X-Requested-With=XMLHttpRequest";
filterContext.Result = new RedirectResult(url);
}
}
}
Recently I ran into exactly the same problem and used the code posted by J. Pablo Fernández
with a modification to account for return URLs. Here it is:
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
override public void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
{
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
HttpRequestBase request = filterContext.HttpContext.Request;
string returnUrl = request.Path;
bool queryStringPresent = request.QueryString.Count > 0;
if (queryStringPresent || request.Form.Count > 0)
returnUrl += '?' + request.QueryString.ToString();
if (queryStringPresent)
returnUrl += '&';
returnUrl += request.Form;
String url = System.Web.Security.FormsAuthentication.LoginUrl +
"?X-Requested-With=XMLHttpRequest&ReturnUrl=" +
HttpUtility.UrlEncode(returnUrl);
filterContext.Result = new RedirectResult(url);
}
}
}
Instead of using the authorize attribute, I've been doing something like the following.
public ActionResult SomeCall(string someData)
{
if (Request.IsAjaxRequest() == false)
{
// TODO: do the intended thing.
}
else
{
// This should only work with AJAX requests, so redirect
// the user to an appropriate location.
return RedirectToAction("Action", "Controller", new { id = ?? });
}
}
I think the right way to handle this would be in your Javascript making the AJAX call.
If the user needs to be authorized (or authenticated as your code implies) and isn't, you should inform them and maybe not allow them to try and comment in the first place.
However, if that doesn't suit your needs.
You could try and write your own authorize action filter, maybe inheriting from the one that comes with the MVC framework but redirects how you want it to. It's fairly straightforward.

Resources