Custom Content-Type validation filter? - asp.net-core

I want to implement a custom Content-Type validation filter so that a custom error model on a 415 Unsupported Media Type can be provided.
Something like this:
public class ValidateContentTypeFilterAttribute : ActionFilterAttribute
{
private const string JsonMimeType = "application/json";
public override void OnActionExecuting(ActionExecutingContext context)
{
string requestMethod = context.HttpContext.Request.Method.ToUpper();
if (requestMethod == WebRequestMethods.Http.Post || requestMethod == WebRequestMethods.Http.Put)
{
if (request.ContentType != JsonMimeType)
{
// "Unsupported Media Type" HTTP result.
context.Result = new HttpUnsupportedMediaTypeResult();
return;
}
}
}
}
The problem is that the MVC pipeline seems to be "catching" unsupported or invalid Content-Type values before executing any custom filters. Even the 'application/xml' content type will be refused.
Where would this be configured?
My MVC configuration consists of not much more than this:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new SquidJsonConverter());
})
.AddMvcOptions(options =>
{
options.Filters.Add(typeof(ValidateAntiForgeryTokenAttribute));
options.Filters.Add(typeof(ValidateContentTypeFilterAttribute));
options.Filters.Add(typeof(ValidateAcceptFilterAttribute));
options.Filters.Add(typeof(ValidateModelFilterAttribute));
});
...
}

Action filters are too late in the processing pipeline for what you are trying to achieve here.
The filter execution order for an "incoming" request is the following:
Authorization filters' OnAuthorization.. method invocation
Resource filters' OnResourceExecuting.. method invocation Model
Model binding happens (this is the place where the content type check is
made)
Action filters' OnActionExecuting.. method invocation
Action execution happens
You could instead create a resource filter. An example:
public class CustomResourceFilter : IResourceFilter
{
private readonly string jsonMediaType = "application/json";
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context.HttpContext.Request.Method == "PUT" || context.HttpContext.Request.Method == "POST")
{
if (!string.Equals(
MediaTypeHeaderValue.Parse(context.HttpContext.Request.ContentType).MediaType,
jsonMediaType,
StringComparison.OrdinalIgnoreCase))
{
context.Result = new JsonResult(new { Error = "An error message here" }) { StatusCode = 415 };
}
}
}
}
If you would like to modify all types of UnsupportedMediaTypeResult responses, then you could write a Result filter instead.
The filter pipeline for outgoing response is:
Action filters' OnActionExecuted... method invocation
Result filters' OnResultExecuting.. method invocation
Result filters' OnResultExecuted.. method invocation
Resource filters' OnResourceExecuted.. method invocation
An example with a Result filter:
public class CustomResultFilter : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var result = context.Result as UnsupportedMediaTypeResult;
if (result != null)
{
context.Result = new JsonResult(new { Error = "An error message here" }) { StatusCode = 415 };
}
}
}

Related

'HttpRequest' does not contain a definition for 'EnableRewind'

I am trying to use the EnableRewind method in a custom authorization handler which I have created but i am getting the error "'HttpRequest' does not contain a definition for 'EnableRewind'" I need to access body in it but if I do it as shown in code I get the error in the controller "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token,...." this is my handler i have injected IHttpContextAccessor from the startup file
public class ForPrivateProfileBodyMustOwnRecordOrShouldBeInAdminRoleHandler : AuthorizationHandler<ForPrivateProfileBodyMustOwnRecordOrShouldBeInAdminRole>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ForPrivateProfileBodyMustOwnRecordOrShouldBeInAdminRoleHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
ForPrivateProfileBodyMustOwnRecordOrShouldBeInAdminRole requirement)
{
var reader = new System.IO.StreamReader(_httpContextAccessor.HttpContext.Request.Body);
var body = reader.ReadToEndAsync().Result;
//this line is producing error
var req = _httpContextAccessor.HttpContext.Request.EnableRewind();
var request = Newtonsoft.Json.JsonConvert.DeserializeObject<PrivateProfileModel>(body);
var ownerId = context.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (request.UserId.ToString() != ownerId && !context.User.IsInRole("Admin"))
{
context.Fail();
return Task.CompletedTask;
}
//all checks pass
//_httpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
context.Succeed(requirement);
return Task.CompletedTask;
}
}
finally i have resolved this issue actually the issue was the particular stream that ASP .NET Core uses –Microsoft.AspNetCore.Server.Kestrel.Internal.Http.FrameRequestStream – is not rewindable (found this article quite helpful http://www.palador.com/2017/05/24/logging-the-body-of-http-request-and-response-in-asp-net-core/) so solved it by creating new stream and placing it in body like this :
var body = reader.ReadToEndAsync().Result;
var request = Newtonsoft.Json.JsonConvert.DeserializeObject<PrivateProfileModel>(body);
using (var injectedRequestStream = new MemoryStream())
{
var bytesToWrite = System.Text.Encoding.UTF8.GetBytes(body);
injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
injectedRequestStream.Seek(0, SeekOrigin.Begin);
_httpContextAccessor.HttpContext.Request.Body = injectedRequestStream;
}

Net Core 3.1 OData returns error 500 when using both $filter and $select and output is xml

I'm currently developing an OData API in .Net Core 3.1 which fetches data from SQL server. Using postman, I'm sending GET requests to the API with Accept headers text/xml and application/json.
With this url: <http://localhost:8008/odata/Contact?$filter=No_ eq 'T20-1234567'&$select=No_> and an application/json Accept-Header (or No Accept-Header) the response is
json response
But with Accept-Header application/xml or text/xml:
An unhandled exception was thrown by the application.
System.ArgumentException: Object of type 'System.Linq.EnumerableQuery1[Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder+SelectSome1[Models.Contact]]' cannot be converted to type 'System.Collections.Generic.IEnumerable`1[Models.Contact]'.
The strange thing is that when the $select part is removed from the url, the request is correctly handled by the application (200).
My controller action:
[HttpGet]
[ODataRoute(nameof(Contact))]
public IQueryable<Contact> GetContact()
{
return _context.Contact;
}
Has anyone seen this type of behaviour?
To answer my own question, here's a possible workaround:
services
.AddMvc(config =>
{
...
config.OutputFormatters.Add(new CustomXmlOutputFormatter());
config.RespectBrowserAcceptHeader = true;
});
public class CustomXmlOutputFormatter : TextOutputFormatter
{
public CustomXmlOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanWriteType(Type type)
{
return true;
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null) throw new ArgumentNullException(nameof(selectedEncoding));
var httpContext = context.HttpContext;
var json = JsonConvert.SerializeObject(new { item = context.Object });
var xml = JsonConvert.DeserializeXNode(json, "root");
var buffer = new StringBuilder(xml.ToString());
await httpContext.Response.WriteAsync(buffer.ToString());
}
}

ServiceStack doesn't populate the response DTO when throwing HttpErrors

ServiceStack doesn't populate the original response in the WebServiceException's responseDTO property.
I'm running the code below which should always return a 404 response code with the ResponseStatus property of the TestResponse populated with "Some bad request" but it also seems like should return the original good response with it's output property populated from the request's input property. However I get null when I look at the WebServiceException responseDTO
public TestResponse Post(Test request)
{
var response = new TestResponse() { Output = request.Input };
throw new HttpError(response, (int)HttpStatusCode.BadRequest, "Some bad request");
}
public TestResponse Get(Test request)
{
try
{
using (var client = new JsonServiceClient("http://localhost:5000"))
{
var response = client.Post(request);
return response;
}
}
catch (WebServiceException ex)
{
throw;
}
}
In general I was expecting that the responseDTO property in the WebServiceException will contain the endpoint's DTO as long as it's passed in when throwing the HttpError but that doesn't seem to be the case. I see only default values and nulls for each property in the responseDTO.
When an Exception is thrown only the ResponseStatus is preserved, you can add any additional info to its Meta dictionary.
Alternatively you can return a failed HTTP Response:
public TestResponse Post(Test request)
{
var response = new TestResponse() { Output = request.Input };
base.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return response;
}

Custom batch handler in asp.net webapi

I want to write custom batch handler in my webapi.
Requirement for this : I am not able to identify weather the incoming request is part of batch or independent.
By writing custom batch handler i will be able to add value in header of each request, which i can use later to identify.
First we need to write custom batch hahttps://stackoverflow.blog/2011/07/01/its-ok-to-ask-and-answer-your-own-questions/ndler
For this we need to override HttpMessageHandler. Below is code
public class BatchHandler : HttpMessageHandler
{
HttpMessageInvoker _server;
public BatchHandler(HttpConfiguration config)
{
// BatchServer is a class which overrides
_server = new HttpMessageInvoker(new BatchServer(config));
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Return 400 for the wrong MIME type
// As batch request will always be of MIME type
if ("multipart/mixed" !=
request.Content.Headers.ContentType.MediaType)
{
return request.CreateResponse(HttpStatusCode.BadRequest);
}
// Start a multipart response
var outerContent = new MultipartContent("batch");
var outerResp = request.CreateResponse();
outerResp.Content = outerContent;
// Read the multipart request
var multipart = await request.Content.ReadAsMultipartAsync();
foreach (var httpContent in multipart.Contents)
{
HttpResponseMessage innerResp = null;
try
{
// Decode the request object
var innerReq = await
httpContent.ReadAsHttpRequestMessageAsync();
innerReq.Headers.Add("IsBatch", "true");
// Send the request through the pipeline
innerResp = await _server.SendAsync(
innerReq,
cancellationToken
);
}
catch (Exception ex)
{
// If exceptions are thrown, send back generic 400
innerResp = new HttpResponseMessage(
HttpStatusCode.BadRequest
);
}
// Wrap the response in a message content and put it
// into the multipart response
outerContent.Add(new HttpMessageContent(innerResp));
}
return outerResp;
}
}
in above code their is this line
// BatchServer is a class which overrides HttpServer
_server = new HttpMessageInvoker(new BatchServer(config));
if we don't do this we gets an error
The 'DelegatingHandler' list is invalid because the property
'InnerHandler' of 'xxhandler' is not null.\r\nParameter
name: handlers
Below is the BatchServer class which overrides HttpServer
public class BatchServer : HttpServer
{
private readonly HttpConfiguration _config;
public BatchServer(HttpConfiguration configuration)
: base(configuration)
{
_config = configuration;
}
protected override void Initialize()
{
var firstInPipeline = _config.MessageHandlers.FirstOrDefault();
if (firstInPipeline != null && firstInPipeline.InnerHandler != null)
{
InnerHandler = firstInPipeline;
}
else
{
base.Initialize();
}
}
}
Now we want to hit batch request on BatchHandler
For this we need configure route to BatchHandler
Add below code to your AppStart
var batchHandler = new BatchHandler(config);
config.Routes.MapHttpRoute("batch", "api/batch", null, null, batchHandler);

Problems implementing ValidatingAntiForgeryToken attribute for Web API with MVC 4 RC

I'm making JSON-based AJAX requests and, with MVC controllers have been very grateful to Phil Haack for his Preventing CSRF with AJAX and, Johan Driessen's Updated Anti-XSRF for MVC 4 RC. But, as I transition API-centric controllers to Web API, I'm hitting issues where the functionality between the two approaches is markedly different and I'm unable to transition the CSRF code.
ScottS raised a similar question recently which was answered by Darin Dimitrov. Darin's solution involves implementing an authorization filter which calls AntiForgery.Validate. Unfortunately, this code does not work for me (see next paragraph) and - honestly - is too advanced for me.
As I understand it, Phil's solution overcomes the problem with MVC AntiForgery when making JSON requests in the absence of a form element; the form element is assumed/expected by the AntiForgery.Validate method. I believe that this may be why I'm having problems with Darin's solution too. I receive an HttpAntiForgeryException "The required anti-forgery form field '__RequestVerificationToken' is not present". I am certain that the token is being POSTed (albeit in the header per Phil Haack's solution). Here's a snapshot of the client's call:
$token = $('input[name=""__RequestVerificationToken""]').val();
$.ajax({
url:/api/states",
type: "POST",
dataType: "json",
contentType: "application/json: charset=utf-8",
headers: { __RequestVerificationToken: $token }
}).done(function (json) {
...
});
I tried a hack by mashing together Johan's solution with Darin's and was able to get things working but am introducing HttpContext.Current, unsure whether this is appropriate/secure and why I can't use the provided HttpActionContext.
Here's my inelegant mash-up.. the change is the 2 lines in the try block:
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
try
{
var cookie = HttpContext.Current.Request.Cookies[AntiForgeryConfig.CookieName];
AntiForgery.Validate(cookie != null ? cookie.Value : null, HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
}
catch
{
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.Forbidden,
RequestMessage = actionContext.ControllerContext.Request
};
return FromResult(actionContext.Response);
}
return continuation();
}
My questions are:
Am I correct in thinking that Darin's solution assumes the existence of a form element?
What's an elegant way to mash-up Darin's Web API filter with Johan's MVC 4 RC code?
Thanks in advance!
You could try reading from the headers:
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
Note: GetCookies is an extension method that exists in the class HttpRequestHeadersExtensions which is part of System.Net.Http.Formatting.dll. It will most likely exist in C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.Formatting.dll
Just wanted to add that this approach worked for me also (.ajax posting JSON to a Web API endpoint), although I simplified it a bit by inheriting from ActionFilterAttribute and overriding the OnActionExecuting method.
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var cookieName = AntiForgeryConfig.CookieName;
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
}
}
}
Extension method using Darin's answer, with a check for the presence of the header. The check means that the resulting error message is more indicative of what's wrong ("The required anti-forgery form field "__RequestVerificationToken" is not present.") versus "The given header was not found."
public static bool IsHeaderAntiForgeryTokenValid(this HttpRequestMessage request)
{
try
{
HttpRequestHeaders headers = request.Headers;
CookieState cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = string.Empty;
if (headers.Any(x => x.Key == AntiForgeryConfig.CookieName))
rvt = headers.GetValues(AntiForgeryConfig.CookieName).FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch (Exception ex)
{
LogHelper.LogError(ex);
return false;
}
return true;
}
ApiController Usage:
public IHttpActionResult Get()
{
if (Request.IsHeaderAntiForgeryTokenValid())
return Ok();
else
return BadRequest();
}
An implementation using AuthorizeAttribute:
using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
public const string HeaderName = "X-RequestVerificationToken";
private static string CookieName => AntiForgeryConfig.CookieName;
public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
if (httpContext == null) {
throw new ArgumentNullException(nameof(httpContext));
}
// check that if the cookie is set to require ssl then we must be using it
if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
}
// try to find the old cookie token
string oldCookieToken = null;
try {
var token = httpContext.Request.Cookies[CookieName];
if (!string.IsNullOrEmpty(token?.Value)) {
oldCookieToken = token.Value;
}
}
catch {
// do nothing
}
string cookieToken, formToken;
AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);
// set the cookie on the response if we got a new one
if (cookieToken != null) {
var cookie = new HttpCookie(CookieName, cookieToken) {
HttpOnly = true,
};
// note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
if (AntiForgeryConfig.RequireSsl) {
cookie.Secure = AntiForgeryConfig.RequireSsl;
}
httpContext.Response.Cookies.Set(cookie);
}
return formToken;
}
protected override bool IsAuthorized(HttpActionContext actionContext) {
if (HttpContext.Current == null) {
// we need a context to be able to use AntiForgery
return false;
}
var headers = actionContext.Request.Headers;
var cookies = headers.GetCookies();
// check that if the cookie is set to require ssl then we must honor it
if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
return false;
}
try {
string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
return false;
}
AntiForgery.Validate(cookieToken, formToken);
return base.IsAuthorized(actionContext);
}
catch {
return false;
}
}
}
Then just decorate your controller or methods with [ApiValidateAntiForgeryToken]
And add to the razor file this to generate your token for javascript:
<script>
var antiForgeryToken = '#ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
If it helps anyone, in .net core, the header's default value is actually just "RequestVerificationToken", without the "__". So if you change the header's key to that instead, it'll work.
You can also override the header name if you like:
services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")