I basically need to protect against Cross Site Request Forgery in my Web API Controller that's a part of an MVC application. I am open to any ideas. At this point, I have an MVC View that displays an Esri map using ArcGIS for JavaScript API. The user creates a route on the map, and the route and various features it crosses can be saved via an AJAX POST. The View does not have a form. This is because all data that I POST to the server is in memory and not visible on screen (or in hidden fields).
So, I have the following in my MVC View to get antiforgery tokens:
@functions{
public string GetTokenHeaderValue()
{
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return cookieToken + ":" + formToken;
}
}
I had this hidden input in the View, but realized this is bad since it has both the 'form token' and cookie token used with AntiForgery.Validation:
<input type="hidden" id="antiforgeryToken" value="@GetTokenHeaderValue()" />
Then, in a separate JavaScript file (not in script tag in View), I am doing a Http POST to my Web API Controller. This is where I add the tokens to the request headers:
var headers = {};
headers['RequestVerificationToken'] = $("#antiforgeryToken").val();
// Ajax POST to Web API Controller
$.ajax({
async: true,
url: '/api/RouteData',
type: 'POST',
headers: headers,
data: requestData,
dataType: 'json',
error: function (xhr, statusText, errorThrown) {
console.log('Error saving route data! ' + errorThrown);
},
success: function (result) {
}
});
NOTE: the data that is getting POSTed in the body is all in memory inside a custom Dojo widget (since the page displays an Esri map using ArcGIS for JavaScript). There is not a form on the page since the user doesn't enter data.)
Tying it all together on the server side in a Web API Controller:
[System.Web.Http.HttpPost]
[ResponseType(typeof(RouteData))]
public async Task<IHttpActionResult> PostRouteData(RouteDataViewModel routeDataVM)
{
try
{
HttpRequestMessage httpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
ValidateRequestHeader(httpRequestMessage);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, ex, ex.Message);
throw;
}
// Now that we know user is who they say they are, perform update
}
void ValidateRequestHeader(HttpRequestMessage request)
{
string cookieToken = "";
string formToken = "";
IEnumerable<string> tokenHeaders;
if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
{
string[] tokens = tokenHeaders.First().Split(':');
if (tokens.Length == 2)
{
cookieToken = tokens[0].Trim();
formToken = tokens[1].Trim();
}
}
AntiForgery.Validate(cookieToken, formToken);
}
AntiForgery.Validate is what validates the tokens.
I've seen this SO post, which has given me some ideas, but didn't quite solve the issue for me. A lot of credit is due to this post on ASP.net as well.
What makes this different for me (I think) is that my JavaScript is in a separate file and cannot call the server-side Razor function to get the antiforgery tokens, right? Any ideas on how to protect against CSRF without a form?