We're hitting a bit of a snag with CORS features of a restful API in ServiceStack 4.
We want to be sneding cookies to the api since the SS session is in the cookies, so we make AJAX calles with "WithCredentials" = true in our angular clients that hit the API.
Since Chrome (at least) doesn't like Access-Control-Allow-Origin wildcards with WithCredentials, we've added a pre-request filter to echo back the requester's origin in the Access-Control-Allow-Origin header like so:
private void ConfigureCors()
{
Plugins.Add(new CorsFeature(
allowedHeaders: "Content-Type",
allowCredentials: true,
allowedOrigins: ""));
PreRequestFilters.Add((httpReq, httpRes) =>
{
string origin = httpReq.Headers.Get("Origin");
if (origin != null)
{
httpRes.AddHeader(HttpHeaders.AllowOrigin, origin);
}
else
{
// Add the dev localhost header.
httpRes.AddHeader(HttpHeaders.AllowOrigin, "http://localhost:9000");
}
});
PreRequestFilters.Add((httpReq, httpRes) =>
{
//Handles Request and closes Responses after emitting global HTTP Headers
if (httpReq.Verb == "OPTIONS")
{
httpRes.EndRequest();
}
});
}
However, we're hitting a snag on OPTIONS requests, because the SS service is not emitting the Access-Control-Allow-Origin header back when the request is ended. This makes Chrome reject the call.
We tried putting an explicit header inside the pre-request filter for OPTIONS, but it still doesn't return an ACAO header for OPTIONS calls:
PreRequestFilters.Add((httpReq, httpRes) =>
{
//Handles Request and closes Responses after emitting global HTTP Headers
if (httpReq.Verb == "OPTIONS")
{
httpRes.AddHeader(HttpHeaders.AllowOrigin, "*");
httpRes.EndRequest();
}
});
It seems like this must have been dealt with before, but we couldn't find anything quite like this on StackOverflow.
Are we doing something wrong with the OPTIONS pre-request filter? Why does it not return an Access-Control-Allow-Origin header?
You should add the whitelisted domains in the allowOriginWhitelist
, e.g:
Plugins.Add(new CorsFeature(allowedHeaders:"Content-Type",
allowCredentials:true,
allowOriginWhitelist:new[]{"http://localhost:9000"}));
There was an issue introduced in v4.0.35 where as PreRequestFilters were being written in Custom HttpHandlers this caused the CorsFeature to write out the Access-Control-Allow-Origin header twice causing browsers to reject it. This has now been resolved in this commit which is available from v4.0.36+ that's now available on MyGet.
This latest version has been deployed to the http://test.servicestack.net demo which shows cross-domain authentication with ServiceStack in this jsbin: http://jsbin.com/korijigucu/1/edit
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<script>
var apiBase = "http://test.servicestack.net";
var app = angular.module('app', []);
app.run(['$rootScope', '$http', function ($rootScope, $http) {
$rootScope.success = "running...";
$http
.post(apiBase + '/auth/credentials',
{ "UserName": "test", "Password": "test" },
{ withCredentials: true })
.success(function (data) {
$rootScope.success = "Login successful: " + JSON.stringify(data);
})
.error(function (data, status, headers, config) {
$rootScope.error = 'ERR:login';
});
}]);
</script>
</head>
<body>
<div style='color:green'>{{success}}</div>
<div style='color:red'>{{error}}</div>
</body>
</html>
Source code for the CorsFeature registration in above Test project:
Plugins.Add(new CorsFeature(
allowOriginWhitelist: new[] {
"http://localhost", "http://localhost:56500",
"http://test.servicestack.net", "http://null.jsbin.com" },
allowCredentials: true,
allowedHeaders: "Content-Type, Allow, Authorization"));
Note: Updating to 4.0.36 resolved the doubled-header issue described below, rendering the second pre-request filter obsolete.
I finally got this to work, but it feels like a kludge.
I added the allowOriginWhitelist entry as Demis suggested, which was returning doubled values for the header (Access-Control-Allow-Origin:http://localhost:9000
), at least for OPTIONS calls (it appears to work fine for POST and GET):
Plugins.Add(new CorsFeature(
allowedHeaders: "Content-Type, Allow, Authorization",
allowCredentials: true,
allowOriginWhitelist: new[] { "http://localhost:9000", "http://www.productiondomain.com", "https://www.productiondomain.com" }));
So I added the following pre-request filter, based on the earlier filter we were using:
PreRequestFilters.Add((httpReq, httpRes) =>
{
//Handles Request and closes Responses after emitting global HTTP Headers
if (httpReq.Verb == "OPTIONS")
{
string origin = httpReq.Headers.Get("Origin");
if (origin != null)
{
httpRes.AddHeader(HttpHeaders.AllowOrigin, origin);
}
else
{
// Add the dev localhost header.
httpRes.AddHeader(HttpHeaders.AllowOrigin, "http://localhost:9000");
}
httpRes.EndRequest();
}
});
This has resolved the issue, but it feels like a kludge. Does anyone know why this simple code would produce doubled ACAO headers in the response?
Plugins.Add(new CorsFeature(
allowedHeaders: "Content-Type, Allow, Authorization",
allowCredentials: true,
allowOriginWhitelist: new[] { "http://localhost:9000", "http://www.productiondomain.com", "https://www.productiondomain.com" }));