Prevent FormsAuthenticationModule of intercepting ASP.NET Web API responses - asp.net

In ASP.NET the FormsAuthenticationModule intercepts any HTTP 401, and returns an HTTP 302 redirection to the login page. This is a pain for AJAX, since you ask for json and get the login page in html, but the status code is HTTP 200.
What is the way of avoid this interception in ASP.NET Web API ?
In ASP.NET MVC4 it is very easy to prevent this interception by ending explicitly the connection:
public class MyMvcAuthFilter:AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest() && !filterContext.IsChildAction)
{
filterContext.Result = new HttpStatusCodeResult(401);
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.SuppressContent = true;
filterContext.HttpContext.Response.End();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
But in ASP.NET Web API I cannot end the connection explicitly, so even when I use this code the FormsAuthenticationModule intercepts the response and sends a redirection to the login page:
public class MyWebApiAuth: AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if(actionContext.Request.Headers.Any(h=>h.Key.Equals("X-Requested-With",StringComparison.OrdinalIgnoreCase)))
{
var xhr = actionContext.Request.Headers.Single(h => h.Key.Equals("X-Requested-With", StringComparison.OrdinalIgnoreCase)).Value.First();
if (xhr.Equals("XMLHttpRequest", StringComparison.OrdinalIgnoreCase))
{
// this does not work either
//throw new HttpResponseException(HttpStatusCode.Unauthorized);
actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
return;
}
}
base.HandleUnauthorizedRequest(actionContext);
}
}
What is the way of avoiding this behaviour in ASP.NET Web API? I have been taking a look, and I could not find a way of do it.
Regards.
PS: I cannot believe that this is 2012 and this issue is still on.

In case someone's interested in dealing with the same issue in ASP.NET MVC app using the Authorize attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class Authorize2Attribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new HttpStatusCodeResult((int) HttpStatusCode.Forbidden);
}
else
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
base.HandleUnauthorizedRequest(filterContext);
}
}
}
This way browser properly distinguishes between Forbidden and Unauthorized requests..

The release notes for MVC 4 RC imply this has been resolved since the Beta - which are you using?
http://www.asp.net/whitepapers/mvc4-release-notes
Unauthorized requests handled by ASP.NET Web API return 401 Unauthroized: Unauthorized requests handled by ASP.NET Web API now return a standard 401 Unauthorized response instead of redirecting the user agent to a login form so that the response can be handled by an Ajax client.
Looking into the source code for MVC there appears to be an functionality added via SuppressFormsAuthRedirectModule.cs
http://aspnetwebstack.codeplex.com/SourceControl/network/forks/BradWilson/AspNetWebStack/changeset/changes/ae1164a2e339#src%2fSystem.Web.Http.WebHost%2fHttpControllerHandler.cs.
internal static bool GetEnabled(NameValueCollection appSettings)
{
// anything but "false" will return true, which is the default behavior
So it looks this this is enabled by default and RC should fix your issue without any heroics... as a side point it looks like you can disable this new module using AppSettings http://d.hatena.ne.jp/shiba-yan/20120430/1335787815:
<appSettings>
<Add Key = "webapi:EnableSuppressRedirect" value = "false" />
</appSettings>
Edit (example and clarification)
I have now created an example for this approach on GitHub. The new redirection suppression requires that you use the two correct "Authorise" attribute's; MVC Web [System.Web.Mvc.Authorize] and Web API [System.Web.Http.Authorize] in the controllers AND/OR in the global filters Link.
This example does however draw out a limitation of the approach. It appears that the "authorisation" nodes in the web.config will always take priority over MVC routes e.g. config like this will override your rules and still redirect to login:
<system.web>
<authentication mode="Forms">
</authentication>
<authorization>
<deny users="?"/> //will deny anonymous users to all routes including WebApi
</authorization>
</system.web>
Sadly opening this up for some url routes using the Location element doesn't appear to work and the WebApi calls will continue to be intercepted and redirected to login.
Solutions
For MVC applications I am simply suggest removing the config from Web.Config and sticking with Global filters and Attributes in the code.
If you must use the authorisation nodes in Web.Config for MVC or have a Hybrid ASP.NET and WebApi application then #PilotBob - in the comments below - has found that sub folders and multiple Web.Config's can be used to have your cake and eat it.

I was able to get around the deny anonymous setting in web.config by setting the following property:
Request.RequestContext.HttpContext.SkipAuthorization = true;
I do this after some checks against the Request object in the Application_BeginRequest method in Global.asax.cs, like the RawURL property and other header information to make sure the request is accessing an area that I want to allow anonymous access to. I still perform authentication/authorization once the API action is called.

Related

How can I manage robots.txt in Azure app service with a staging slot

I have an ASP.NET MVC app running in an Azure app service with one staging slot, and a build and release pipeline in VSTS.
I want the production instance to have Allow / in robots.txt and Disallow / in the staging slot at all times.
Currently we are changing robots.txt manually every time we do a swap but this is error prone
How can I automate this process?
To solve this problem I did consider creating the robots.txt file dynamically based on app settings set in the Azure portal (set to stay with the slot), however this won't work since after the swap happens prod will have the staging Disallow rule.
Can anyone advise the best way to manage this?
Robots are mainly used by search engines to crawl and check pages on the public websites. Staging and other deployment slots are not public (and should not be public — unless you have a good reason for that), and thus it doesn't make much sense to configure and manage it. Secondly, in most cases I would recommend to redirect any public request to your production slot and keep staging offline and active for internal use cases only. This would also help you to manage the analytics and logs coming from the public only, and not being polluted with internal and deployment slots stuff.
Anyways, if you are still inclined to do this, then there is one way that you can manage this. Write your own routing to control the robots file, and then render a content-type: text/plain page, which would be dynamic based on whether it is a staging or production request. Something like this,
// Create the robots.txt file dynamically, by controlling the URL handler
[Route("robots.txt")]
public ContentResult DynamicRobotsFile()
{
StringBuilder content = new StringBuilder();
content.AppendLine("user-agent: *");
// Check the condition by URL or Environment variable
if(allow) {
content.AppendLine("Allow: /");
else {
content.AppendLine("Disallow: /");
}
return this.Content(stringBuilder.ToString(), "text/plain", Encoding.UTF8);
}
This way you can manage how the robots.txt is created and you would be able to control the allow disallow for the robots. You can create a separate controller or an action only in the home controller of your app.
Now that you know how to do, you can setup the environment variables for the production/staging slots to check other requirements.
I use below code and It works for me
[Route("robots.txt")]
public ContentResult DynamicRobotsFile()
{
StringBuilder content = new StringBuilder();
if (System.Configuration.ConfigurationManager.AppSettings["production"] != "true")
{
content.AppendLine("user-agent: *");
content.AppendLine("Disallow: /");
}
return this.Content(content.ToString(), "text/plain", Encoding.UTF8);
}
web.config
<appSettings>
<add key="production" value="false" />
</appSettings>
<system.webServer>
<handlers>
<add name="RobotsTxt" path="robots.txt" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
EDITED
I use this version now.
[Route("/robots.txt")]
public ContentResult RobotsTxt()
{
var sb = new StringBuilder().AppendLine("User-Agent: *");
if (_env.IsProduction())
{
sb.AppendLine("Allow: /");
sb.AppendLine("Disallow: /admin");
}
else
{
sb.AppendLine("Disallow: /");
}
sb.AppendLine(string.Empty);
sb.AppendLine($"Sitemap: {this.Request.Scheme}://{this.Request.Host}/sitemap.xml");
return this.Content(sb.ToString(), "text/plain", Encoding.UTF8);
}
and I use IWebHostEnvironment to detect prod or not
public class SeoController : Controller
{
private readonly IWebHostEnvironment _env;
public SeoController(IWebHostEnvironment env)
{
_env = env;
}
}

ASP.NET Web API not returning custom error information

I have an ASP.NET Web API 2 action method:
[System.Web.Http.HttpPost]
public HttpResponseMessage Create(HttpRequestMessage req)
{
//...
if (success)
return Request.CreateResponse(HttpStatusCode.Created);
return CreateErrorResponse(HttpStatusCode.BadRequest, "you done bad");
}
Until I did "something", upon error it would return http 400 with the custom error text "you done bad". That's the expected result.
It no longer returns the custom text; it just returns the standard "Bad request".
Have been trying to understand what changed to make this happen.
So I tried:
var response = new { message = "you done bad" };
return Request.CreateResponse(HttpStatusCode.BadRequest, response);
Same result.
I then created a new, clean Web API project and I get the result I expect.
How did I break my project?
The problem is related to the CustomErrors configuration in web.config. From #HaukurHaf's answer at Error messages returned from Web API method are omitted in non-dev environment:
Had the same issue. It's indeed because of the custom errors setting.
In a real world scenario, you would definitely want to use a custom
error page in your application, but in order for custom exception
messages to work in the WebAPI you need to disable the custom errors
page.
How to fix this? Luckily, you can use the <location> element in your
web.config to solve this.
Solution:
<!-- General for the application -->
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="YourCustomErrorPage.aspx"/>
</system.web>
<!-- Override it for paths starting with api (your WebAPI) -->
<location path="api">
<system.web>
<customErrors mode="Off" />
</system.web>
</location>
I use this method in my own app, works well.
While I do see that this works, I don't understand why the CustomErrors section breaks it.
You need to change your method as below
[Route("User/GetUserIDCustomMessage")]
[HttpPost]
public HttpResponseMessage GetUserIDCustomMessage(HttpRequestMessage request, int Id)
{
HttpResponseMessage response = null;
if (Id == 1)
{
response = request.CreateResponse(HttpStatusCode.OK);
}
else {
response = request.CreateResponse(HttpStatusCode.BadRequest , "YOu type bad");
}
return response;
}
When I give 1 , it will give me OK message with 200 status code , otherwise it will give 400/Bad Request , with body message 'YOu type bad'
I think you can probably consider creating a instance of HttpResponseMessage and add your custom error message to its Content property.
HttpResponseMessage msg = new HttpResponseMessage(HttpStatusCode.BadRequest);
msg.Content = new StringContent("you done bad");
return msg;
// or can re-throw it
//throw new HttpResponseException(msg);
Please check the following link for details
https://msdn.microsoft.com/en-us/library/system.net.http.httpresponsemessage(v=vs.118).aspx

How to url redirect/rewrite .asp into .aspx

I'm running in the Cassini developer server inside Visual Studio 2012, and I need to redirect clients from the legacy .asp pages to .aspx pages.
Note: Ideally I would redirect clients from .asp to a friendly url, and then internally do a rewrite to .aspx
POST /ResetClock.asp
HTTP/1.1 307 Temporary Redirect
Location: //stackoverflow.us/ResetClock
And then internally:
POST /ResetClock
rewrites into /ResetClock.ashx (Yes, I changed it to .ashx; that's the virtue of url rewriting).
Like what Hanselman did
This is a lot like what Scott Hanselman did:
Request for /foo.html
gives the client a redirect to /foo
client request for /foo
is re-written into /foo.html
The attempted hack
I tried the hack solution; alter the .asp page to force a redirect to the .ashx (and live to fight with the url re-write syntax another day):
ResetClock.asp
<%
Response.Redirect("ResetClock.aspx")
Response.End
%>
Except that Cassini does not serve .asp pages at all:
This type of page is not served.
Description: The type of page you have requested is not served because it has been explicitly forbidden. The extension '.asp' may be incorrect. Please review the URL below and make sure that it is spelled correctly.
Requested URL: /WebSite/FetchTimes.asp
Which points to a related issue. The solution I end up using cannot require anything that isn't already available on the IIS7.5. And it cannot require anything that needs access to the IIS Admin tools; and must exist entirely within the web-site (e.g. the web.config).
The question
How do I re-write .asp into something more ASP.net-ish?
Edit: Changed GET to a POST to thwart nitpickers who wonder why the 307 Temporary Redirect and not 302 Found or 303 See Other.
The solution is to create an IHttpModule. HttpModules let you intercept every request, and react as you desire.
The first step is to create the plumbing of an IHttpModule:
class UrlRewriting : IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(this.Application_BeginRequest);
application.EndRequest += new EventHandler(this.Application_EndRequest);
}
public void Dispose()
{
//Nothing to do here
}
private void Application_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
}
private void Application_EndRequest(object sender, EventArgs e)
{
}
}
And then register our HttpHandler in the web.config file:
web.config:
<configuration>
<system.web>
<httpModules>
<add name="UrlRewriting" type="UrlRewriting"/>
</httpModules>
</system.web>
</configuration>
Now we have a method (Application_BeginRequest) that will run every time a request is made.
Issue client redirect if they ask for ASP page
The first order of business is redirect the client to a "clean" form. For example, a request for /File.asp is redirected to /File:
private void Application_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
//Redirct any requests to /File.asp into a /File
if (context.Request.Url.LocalPath == VirtualPathUtility.ToAbsolute("~/File.asp"))
{
//Be sure to issue a 307 Temporary Redirect in case the client issued a POST (i.e. a non-GET)
//If we issued 302 Found, a buggy client (e.g. Chrome, IE, Firefox) might convert the POST to a GET.
//If we issued 303 See Other, the client is required to convert a POST to a GET.
//If we issued 307 Temporary Redirect, the client is required to keep the POST method
context.Response.StatusCode = (int)HttpStatusCode.TemporaryRedirect;
context.Response.RedirectLocation = VirtualPathUtility.ToAbsolute("~/File");
context.Response.End();
}
}
And then the internal rewrite
Now that the client will be asking for /File, we have to re-write that internally to an .aspx, or in my case, an .ashx file:
private void Application_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
//Redirct any requests to /ResetClock.asp into a /File
if (context.Request.Url.LocalPath == VirtualPathUtility.ToAbsolute("~/ResetClock.asp"))
{
//Be sure to issue a 307 Temporary Redirect in case the client issued a POST (i.e. a non-GET)
//If we issued 302 Found, the buggy client might convert the POST to a GET.
//If we issued 303 See Other, the client is required to convert a POST to a GET.
//If we issued 307 Temporary Redirect, the client is required to keep the POST method
context.Response.StatusCode = (int)HttpStatusCode.TemporaryRedirect;
context.Response.RedirectLocation = VirtualPathUtility.ToAbsolute("~/ResetClock");
context.Response.End();
}
//Rewrite clean url into actual handler
if (context.Request.Url.LocalPath == VirtualPathUtility.ToAbsolute("~/ResetClock"))
{
String path = "~/ResetClock.ashx"; //no need to map the path
context.Server.Execute(path, true);
//The other page has been executed
//Do not continue or we will hit the 404 of /ResetClock not being found
context.Response.End();
}
}
IIS contains some basic url redirection
Starting with some unknown version of IIS, they added a (now mocked) form of URL Rewriting. It doesn't issuing a client redirect, only an internal re-write. But at least it could be used to solve my problem (responding to an ASP page with ASP.net content):
web.config
<configuration>
<system.web>
<urlMappings>
<add url="~/ResetClock.asp" mappedUrl="~/ResetClock.ashx"/>
</urlMappings>
</system.web>
</configuration>
The client will still appear to have found a resource at /ResetClock.asp, but the guts of the response will have come from /ResetClock.ashx.
Note: Any code is released into the public domain. No attribution required.

ASP.NET MVC4 error handling for 404, 401 and other exceptions

I'm struggling to understand how to correctly handle errors in ASP.NET MVC4. As an example, I've created a new MVC4 project using the "Internet Application" template and updated my home controller to test out some error cases:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Hello";
return View();
}
public ActionResult About()
{
throw new HttpException(401, "Not Authorized");
}
public ActionResult Contact()
{
throw new Exception("Oh no, some error occurred...");
}
}
I have enabled customErrors in my web.config file:
<customErrors mode="On"></customErrors>
When I run the app and click "Contact", I see the ~/Views/Shared/Error.cshtml view as expected, since I have the HandleErrorAttribute registered as a global filter.
However, when I click "About", I get the standard ASP.NET yellow error page that says "Runtime Error". Why are these two exceptions being handled differently and how can I get instances of HttpException to get caught using the HandleError attribute?
CustomErrors config
Ideally, I'd like custom error pages for the following:
A custom 404 (not found) page that's nice and user friendly
A custom 401 (not authorised) page that informs the user that they do not have access (e.g. thrown after checking permissions for a particular item in the model)
A generic error page that is used in all other cases (in place of the standard yellow ASP.NET page).
I've created a new "Error" controller with views for each of the scenarios above. I have then updated customErrors in web.config like so:
<customErrors mode="On" defaultRedirect="~/Error/Trouble">
<error statusCode="404" redirect="~/Error/NotFound"></error>
<error statusCode="401" redirect="~/Error/NotAuthorized"></error>
</customErrors>
The 404 page works fine, but I don't get the 401 page at all. Instead, I get the ~/Error/Trouble view (the one specified as the defaultRedirect) when I try to access the About action on the Home controller.
Why is my custom 401 redirect page not working?
ASP.NET uses 401's internally to redirect users to the login page. Wherever you were planning to throw a 401 unauthorized, instead throw a 403 forbidden.
If you really need to return a 401 and not a 403, then you can use:
HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true
I had a similar problem where I could not get 401 errors to go to my page despite the change to the web.config.
For a 401 you will probably be seeing the standard 401 Unauthorised page, even if you have added 401 to the customerrors section in your web.config. I read that when using IIS and Windows Authentication the check happens before ASP.NET even sees the request, hence you see it's own 401.
For my project I edited the Global.asax file to redirect to a route I had created for 401 errors, sending the user to the "Unauthorised to see this" view.
In the Global.asax:
void Application_EndRequest(object sender, System.EventArgs e)
{
// If the user is not authorised to see this page or access this function, send them to the error page.
if (Response.StatusCode == 401)
{
Response.ClearContent();
Response.RedirectToRoute("ErrorHandler", (RouteTable.Routes["ErrorHandler"] as Route).Defaults);
}
}
and in the Route.config:
routes.MapRoute(
"ErrorHandler",
"Error/{action}/{errMsg}",
new { controller = "Error", action = "Unauthorised", errMsg = UrlParameter.Optional }
);
and in the controller:
public ViewResult Unauthorised()
{
//Response.StatusCode = 401; // Do not set this or else you get a redirect loop
return View();
}

Adding customized content on login page

Most of my ASP.NET website is accessible to an anonymous web users. However there are a few pages that I require authentication before I allow access. I control this via the web.config:
<authorization>
<allow users="*"/>
</authorization>
Currently my logon.aspx file is generic, but I would really like to include instructions telling the user why he was redirected to the logon page. Something like:
Before you can volunteer for a task, please logon so the system can identify you.
OR
You've attempted to edit an event. The system only allows Administrators to do this. Please logon so we can verify you are an administrator of this event.
The instructions on the logon page would depend on what the user was attempting prior to the forms authentication redirect.
My problem is how can the logon page determine what action was taken? Is there anyway to pass a custom querystring to the logon page? I guess I could decode the ReturnUrl and use that to try an determine what instructions to display. However that approach just feels....dirty. I don't like the logon page having a dependency on the URL names of other pages in the system.
Any suggestions?
You could use the HttpRequest.UrlReferrer Property to find out what was the user intention.
Or you could set a QueryString / Session variable before redirecting and use it to display some message.
Why dont you use custom exceptions to do this.
public class ClassName: Exception
{
protected ClassName() : base() {}
public ClassName(String message) : base(message) { }
}
Then you do
public class First: ClassName
{
public First() : base("message") {}
}
public class Second: ClassName
{
public Second() : base("message") { }
}
Now you just catch exceptions of type ClassName and get the message value and pass it into an asp label or textbox or however you wish to do it
You can also capture the redirect in global.asax's Application.EndRequest
void Application_EndRequest(object sender, EventArgs e)
{
if (!HttpContext.Current.Request.IsAuthenticated && HttpContext.Current.Response.StatusCode == 302 && HttpContext.Current.Response.RedirectLocation != null && HttpContext.Current.Response.RedirectLocation.ToLower().Contains("login.aspx"))
{
// do some switching or determining here, or have items preset in your HttpContext.Current.Items[]
HttpContext.Current.Response.RedirectLocation += "&Action=blah";
}
}

Resources