I have an ASP.NET WebAPI (v2) controller action that has the following general structure:
[HttpPost]
public HttpResponseMessage Post(UserDTO model)
{
try {
// do something
}
catch (Exception ex)
{
var error = new {
errorMessage = ex.Message,
userId = 123,
// some other simple data
};
return Request.CreateResponse(HttpStatusCode.BadRequest, error);
}
return Request.CreateResponse(HttpStatusCode.OK, model);
}
When I run this on my local development server (IIS Express) and an error is thrown, I get the expected JSON payload back.
{
config: {...},
data: {
errorMessage: "User invalid",
userId: 123,
...
},
status: 400,
statusText: "Bad Request"
}
When I run the same code/data on the remote/production server (IIS 8.5), all I get back is:
{
config: {...},
data: "Bad Request,
status: 400,
statusText: "Bad Request"
}
The custom data payload is lost/stripped away from the response. This appears to be related to the HttpStatusCode used in the Request.CreateResponse() call as if I change HttpStatusCode.BadRequest to HttpStatusCode.OK then the custom data payload is downloaded.
As I test, I tried changing the return to Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); but the results were the same, i.e. the data was returned as a simple "Bad Request" string.
For reference, the API is being called by an AngularJS $http.post() call.
Why is the change in HttpStatusCode changing the response payload on the production server but not locally? Any help would be much appreciated.
It turns out this was down to the following section in Web.config
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="403" />
<error statusCode="403" responseMode="ExecuteURL"
path="/Error/AccessDenied" />
<remove statusCode="404" />
<error statusCode="404" responseMode="ExecuteURL"
path="/Error/NotFound" />
<remove statusCode="500" />
<error statusCode="500" responseMode="ExecuteURL"
path="/Error/ApplicationError" />
</httpErrors>
</system.webServer>
It was 'odd' because these pages were not being returned by the API call, but with their removal, the API call returned the correct payload - presumably the HttpStatusCode.BadRequest was being intercepted by an error handler somewhere hence losing the original response data?
With these handlers I removed, I resorted to using the Application_Error handler in Global.asax as described by ubik404 here.
There may well be a better/alternative way to achieve the same result, but this seems to work.
You need to remove existingResponse="Replace", I just had the same issue and that solved it for me. The default value is Auto.
Your answer lead me to find the real problem, so thanks! :)
Documentation
I recently implemented XSRF on MVC/angularjs. The secure site is meant to be loaded in 2 ways, either through a direct post or in an iframe.
here is the code below:-
ANGULAR
.config(function ($httpProvider) {
$httpProvider.defaults.xsrfCookieName = '#model.Handlers.XsrfHandler.COOKIE_HEADER_NAME';
$httpProvider.defaults.xsrfHeaderName = '#model.Handlers.XsrfHandler.COOKIE_HEADER_NAME';
})
MVC - filter
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
var response = filterContext.HttpContext.Response;
//TODO : Check the current value of the cookie if it exists
if (request.HttpMethod != "GET")
{
base.OnResultExecuting(filterContext);
return;
}
//TODO : SHA hash the cookie or something.
var cookieValue = filterContext.HttpContext.Session.SessionID;
if (!request.Cookies.AllKeys.Contains(AntiForgeryConfig.CookieName))
{
var cookie = new HttpCookie(XsrfHandler.COOKIE_HEADER_NAME, cookieValue) { HttpOnly = false };
response.SetCookie(cookie);
}
else
{
response.Cookies[XsrfHandler.COOKIE_HEADER_NAME].Value = cookieValue;
}
base.OnResultExecuting(filterContext);
}
MVC- handler
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!xsrfMethods.Contains(request.Method.Method))
return await base.SendAsync(request, cancellationToken);
var cookie = request.Headers.GetCookies().SelectMany(chv => chv.Cookies).FirstOrDefault(cs => cs.Name == COOKIE_HEADER_NAME);
if (cookie == null)
return CreateError(request);
var headerValue = request.Headers.GetValues(COOKIE_HEADER_NAME).FirstOrDefault();
if (string.IsNullOrWhiteSpace(cookie.Value) || string.IsNullOrWhiteSpace(headerValue))
return CreateError(request);
if (headerValue.Trim().Equals(cookie.Value.Trim(), System.StringComparison.InvariantCulture))
return await base.SendAsync(request, cancellationToken);
return CreateError(request);
}
Now we have a client who is loading the HTTPS iframe on an HTTP site (dunno why). It still works fine in chrome, firefox but fails in IE. It would seems that the XSRF token is not getting attached to POST request header for IE.
My question is, what are the workaround for the header problem without have to add any edge case logic.
I was just being dumb. the answer was given in several post.
Just needed to add the P3P protocol for IE.
Added P3P to the web.config and voila.
<httpProtocol>
<customHeaders>
<add name="P3P" value="CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"" />
</customHeaders>
</httpProtocol>
I have a WebAPI 2 REST service running with Windows Authentication. It is hosted separately from the website, so I've enabled CORS using the ASP.NET CORS NuGet package. My client site is using AngularJS.
So far, here's what I've been through:
I didn't have withCredentials set, so the CORS requests were returning a 401. Resolved by adding withCredentials to my $httpProvider config.
Next, I had set my EnableCorsAttribute with a wildcard origin, which isn't allowed when using credentials. Resolved by setting the explicit list of origins.
This enabled my GET requests to succeed, but my POST issued a preflight request, and I hadn't created any controller actions to support the OPTIONS verb. To resolve this, I've implemented a MessageHandler as a global OPTIONS handler. It simply returns 200 for any OPTIONS request. I know this isn't perfect, but works for now, in Fiddler.
Where I'm stuck - my Angular preflight calls aren't including the credentials. According to this answer, this is by design, as OPTIONS requests are designed to be anonymous. However, the Windows Authentication is stopping the request with a 401.
I've tried putting the [AllowAnonymous] attribute on my MessageHandler. On my dev computer, it works - OPTIONS verbs do not require authentication, but other verbs do. When I build and deploy to the test server, though, I am continuing to get a 401 on my OPTIONS request.
Is it possible to apply [AllowAnonymous] on my MessageHandler when using Windows Authentication? If so, any guidance on how to do so? Or is this the wrong rabbit hole, and I should be looking at a different approach?
UPDATE:
I was able to get it to work by setting both Windows Authentication and Anonymous Authentication on the site in IIS. This caused everything to allow anonymous, so I've added a global filter of Authorize, while retaining the AllowAnonymous on my MessageHandler.
However, this feels like a hack...I've always understood that only one authentication method should be used (no mixed). If anyone has a better approach, I'd appreciate hearing about it.
I used self-hosting with HttpListener and following solution worked for me:
I allow anonymous OPTIONS requests
Enable CORS with SupportsCredentials set true
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
var listener = appBuilder.Properties["System.Net.HttpListener"] as HttpListener;
if (listener != null)
{
listener.AuthenticationSchemeSelectorDelegate = (request) => {
if (String.Compare(request.HttpMethod, "OPTIONS", true) == 0)
{
return AuthenticationSchemes.Anonymous;
}
else
{
return AuthenticationSchemes.IntegratedWindowsAuthentication;
}};
}
I have struggled for a while to make CORS requests work within the following constraints (very similar to those of the OP's):
Windows Authentication for all users
No Anonymous authentication allowed
Works with IE11 which, in some cases, does not send CORS preflight requests (or at least do not reach global.asax BeginRequest as OPTIONS request)
My final configuration is the following:
web.config - allow unauthenticated (anonymous) preflight requests (OPTIONS)
<system.web>
<authentication mode="Windows" />
<authorization>
<allow verbs="OPTIONS" users="*"/>
<deny users="?" />
</authorization>
</system.web>
global.asax.cs - properly reply with headers that allow caller from another domain to receive data
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Context.Request.HttpMethod == "OPTIONS")
{
if (Context.Request.Headers["Origin"] != null)
Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, MaxDataServiceVersion");
Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
Response.End();
}
}
CORS enabling
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// all requests are enabled in this example. SupportsCredentials must be here to allow authenticated requests
var corsAttr = new EnableCorsAttribute("*", "*", "*") { SupportsCredentials = true };
config.EnableCors(corsAttr);
}
}
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
This is a much simpler solution -- a few lines of code to allow all "OPTIONS" requests to effectively impersonate the app pool account. You can keep Anonymous turned Off, and configure CORS policies per normal practices, but then add the following to your global.asax.cs:
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Context.Request.HttpMethod == "OPTIONS" && Context.User == null)
{
Context.User = System.Security.Principal.WindowsPrincipal.Current;
}
}
In our situation:
Windows Authentication
Multiple CORS origins
SupportCredentials set to true
IIS Hosting
we found that the solution was elsewhere:
In Web.Config all we had to do was to add runAllManagedModulesForAllRequests=true
<modules runAllManagedModulesForAllRequests="true">
We ended up to this solution by looking into a solution on why the Application_BeginRequest was not being triggered.
The other configurations that we had:
in Web.Config
<authentication mode="Windows" />
<authorization>
<allow verbs="OPTIONS" users="*" />
<deny users="?"/>
</authorization>
in WebApiConfig
private static string GetAllowedOrigins()
{
return ConfigurationManager.AppSettings["CorsOriginsKey"];
}
public static void Register(HttpConfiguration config)
{
//set cors origins
string origins = GetAllowedOrigins();
var cors = new EnableCorsAttribute(origins, "*", "*");
config.EnableCors(cors);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
BTW "*" cors origin is not compatible with Windows Authentication / SupportCredentials = true
https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#pass-credentials-in-cross-origin-requests
I solved it in a very similar way but with some details and focused on oData service
I didn't disable anonymous authentication in IIS since i needed it to POST request
And I've added in Global.aspx (Adding MaxDataServiceVersion in Access-Control-Allow-Headers) the same code than above
protected void Application_BeginRequest(object sender, EventArgs e)
{
if ((Context.Request.Path.Contains("api/") || Context.Request.Path.Contains("odata/")) && Context.Request.HttpMethod == "OPTIONS")
{
Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,MaxDataServiceVersion");
Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
Context.Response.End();
}
}
and WebAPIConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
and AngularJS call
$http({
method: 'POST',
url: 'http://XX.XXX.XXX.XX/oData/myoDataWS.svc/entityName',
withCredentials: true,
headers: {
'Content-Type': 'application/json;odata=verbose',
'Accept': 'application/json;odata=light;q=1,application/json;odata=verbose;q=0.5',
'MaxDataServiceVersion': '3.0'
},
data: {
'#odata.type':'entityName',
'field1': 1560,
'field2': 24,
'field3': 'sjhdjshdjsd',
'field4':'wewewew',
'field5':'ewewewe',
'lastModifiedDate':'2015-10-26T11:45:00',
'field6':'1359',
'field7':'5'
}
});
Dave,
After playing around with the CORS package, this is what caused it to work for me: [EnableCors(origins: "", headers: "", methods: "*", SupportsCredentials=true)]
I had to enable SupportsCredentials=true. Origins,Headers, and Methods are all set to "*"
disable anonymous authentication in IIS if you don't need it.
Than add this in your global asax:
protected void Application_BeginRequest(object sender, EventArgs e)
{
if ((Context.Request.Path.Contains("api/") || Context.Request.Path.Contains("odata/")) && Context.Request.HttpMethod == "OPTIONS")
{
Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
Context.Response.End();
}
}
Make sure that where you enable cors you also enable the credential usage, like:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
As you can see I enable CORS globally and using the application BeginRequest hook I authenticate all the OPTIONS requests for the api (Web Api) and the odata requests (if you use it).
This works fine with all browsers, in the client side remember to add the xhrFiled withCredentials like shown below.
$.ajax({
type : method,
url : apiUrl,
dataType : "json",
xhrFields: {
withCredentials: true
},
async : true,
crossDomain : true,
contentType : "application/json",
data: data ? JSON.stringify(data) : ''
}).....
I'm trying to find another solution avoiding to use the hook but without success until now,
I would use the web.config configuration to do something like the following:
WARNING THE CONFIGURATION BELOW DOESN'T WORK!
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<authentication mode="Windows" />
<authorization>
<deny verbs="GET,PUT,POST" users="?" />
<allow verbs="OPTIONS" users="?"/>
</authorization>
</system.web>
<location path="api">
<system.web>
<authorization>
<allow users="?"/>
</authorization>
</system.web>
</location>
Other solutions I found on the web didn't work for me or seemed too hacky; in the end I came up with a simpler and working solution:
web.config:
<system.web>
...
<authentication mode="Windows" />
<authorization>
<deny users="?" />
</authorization>
</system.web>
Project properties:
Turn on Windows Authentication
Turn off Anonymous Authentication
Setup CORS:
[assembly: OwinStartup(typeof(Startup))]
namespace MyWebsite
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
This requires Microsoft.Owin.Cors assembly that is avaliable on NUget.
Angular initialization:
$httpProvider.defaults.withCredentials = true;
This is my solution.
Global.asax*
protected void Application_BeginRequest(object sender, EventArgs e)
{
if(!ListOfAuthorizedOrigins.Contains(Context.Request.Headers["Origin"])) return;
if (Request.HttpMethod == "OPTIONS")
{
HttpContext.Current.Response.Headers.Remove("Access-Control-Allow-Origin");
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
HttpContext.Current.Response.StatusCode = 200;
HttpContext.Current.Response.End();
}
if (Request.Headers.AllKeys.Contains("Origin"))
{
HttpContext.Current.Response.Headers.Remove("Access-Control-Allow-Origin");
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
}
}
I have this webapi method here:
// PUT api/Competitions/5
public HttpResponseMessage PutCompetitor(int id, CompetitionViewModel competitorviewmodel)
{
...
}
The CompetitionViewModel looks something like this:
public class CompetitionViewModel
{
public int CompetitorId { get; set; }
public string Owned{ get; set; }
public bool Sold { get; set; }
}
I have an angular http.put call to update a competition model that looks like this:
$scope.updateProject = function () {
$http.put(mvc.base + "API/Projects/" + masterScopeTracker.ProjectID, $scope.ProjectCRUD)
.success(function (result) {
})
.error(function (data, status, headers, config) {
masterScopeTracker.autoSaveFail;
});
}
On page load, a new competition is created. So I have a model like the following:
{
CompetitorId: 56,
Owned: null,
Sold: false
}
Every 15 seconds a call to update the model is made. If I don't change any of the values of the model, the webapi put method gets called and runs successfully without a problem. If I change the model to this:
{
CompetitorId: 56,
Owned: "Value",
Sold: false
}
I get a 500 Error and the method is not hit. Not understanding what I am doing wrong here. The view model accepts a string. A string is being sent in the payload. Yet I get the error. Anyone have any ideas?
UPDATE:
I was able to get the server to give me this error:
{"Message":"Anerrorhasoccurred.",
"ExceptionMessage":"Objectreferencenotsettoaninstanceofanobject.",
"ExceptionType":"System.NullReferenceException",
"StackTrace":"atClientVisit.Models.ClientVisitEntities.SaveChanges()\r\natClientVisit.Controllers.API.CompetitionsController.PutCompetitor(Int32id,CompetitionViewModelcompetitorviewmodel)\r\natlambda_method(Closure,Object,Object[])\r\natSystem.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.<GetExecutor>b__c(Objectinstance,Object[]methodParameters)\r\natSystem.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Objectinstance,Object[]arguments)\r\natSystem.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1func,CancellationTokencancellationToken)"
}
I should also say that this doesn't happen locally. This only happens when deployed on the clients server.
You should check the event log to see what the actual error is on the server side. I've had trouble with IIS/IIS Express before with Put because WebDAV was enabled. You can disable it in your web.config:
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule" />
</modules>
<handlers>
<remove name="WebDAV" />
</handlers>
</system.webServer>
I'm very much new to silverlight, so please assume I've done something very daft....
I am trying to make a call from a silverlight app to a WCF service and check a value in the session. The value will have been put there by an aspx page. It's a little convoluted, but that's where we are.
My service looks like this:
[ServiceContract]
public interface IExportStatus
{
[OperationContract]
ExportState RequestExportComplete();
}
public enum ExportState
{
Running,
Complete
}
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class ExportStatus : IExportStatus
{
ExportState IExportStatus.RequestExportComplete()
{
// check value of session variable here.
}
}
The site that hosts the silverlight app also hosts the wcf service. Its web config looks like this:
<configuration>
<system.serviceModel>
<services>
<service name="SUV_MVVM.Web.Services.ExportStatus" behaviorConfiguration="MyBehavior">
<endpoint binding="basicHttpBinding"
bindingConfiguration="MyHttpBinding"
contract="SUV_MVVM.Web.Services.IExportStatus"
address="" />
</service>
</services>
<bindings>
<basicHttpBinding>
<binding name="MyHttpBinding" />
</basicHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="MyBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
I added the service reference to my silverlight app using the VS tooling acepting the defaults (apart for the namespace)
Initially I was just trying to call the service like this:
var proxy = new ExportStatusClient();
proxy.RequestExportCompleteCompleted += (s, e) =>
{
//Handle result
};
proxy.RequestExportCompleteAsync();
But the session in the service was always empty (not null, just empty), so I tried manually setting the session Id into the request like this:
var proxy = new ExportStatusClient();
using (new OperationContextScope(proxy.InnerChannel))
{
var request = new HttpRequestMessageProperty();
//this might chnage if we alter the cookie name in the web config.
request.Headers["ASP.NET_SessionId"] = GetCookie("ASP.NET_SessionId");
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = request;
proxy.RequestExportCompleteCompleted += (s, e) =>
{
//Handle result
};
proxy.RequestExportCompleteAsync();
}
private string GetCookie(string key)
{
var cookies = HtmlPage.Document.Cookies.Split(';');
return (from cookie in cookies
select cookie.Split('=')
into keyValue
where keyValue.Length == 2 && keyValue[0] == key
select keyValue[1]).FirstOrDefault();
}
But what I'm finding is that the HtmlPage.Document.Cookies property is always empty.
So Am I just missing something really basic, or are there any other things that I need to change or test?
I just did a test from a Silverlight 4 application.
System.Windows.Browser.HtmlPage.Document.Cookies = "KeyName1=KeyValue1;expires=" + DateTime.UtcNow.AddSeconds(10).ToString("R");
System.Windows.Browser.HtmlPage.Document.Cookies = "KeyName2=KeyValue2;expires=" + DateTime.UtcNow.AddSeconds(60).ToString("R");
Each cookie expired as expected.
The value of System.Windows.Browser.HtmlPage.Document.Cookies...
Immediately after setting the cookies: "KeyName1=KeyValue1; KeyName2=KeyValue2"
30 seconds later: "KeyName2=KeyValue2"
60+ seconds later: ""