Asp WebApi: Prettify ActionResult JSON output - c#

I'm working on REST service based on ASP.NET Core Web API and want to add a parameter 'prettify' to my endpoint so that response json will be formatted with indentation and readable in web browser.
My question - how can I change JSON formatting per controller method in ASP.WEB API Core application?
Appreciate you help.

Thank to #Nkosi comment, I've found the solution. Below is a code of the action filter that looks for 'prettify' parameter and adds indentation to output JSON. If the parameter omitted, indentation is also added.
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var actionResult = context.Result as ObjectResult;
if (actionResult == null) return;
var paramObj = context.HttpContext.Request.Query["prettify"];
var isPrettify = string.IsNullOrEmpty(paramObj) || bool.Parse(paramObj);
if (!isPrettify) return;
var settings = new JsonSerializerSettings { Formatting = Formatting.Indented };
actionResult.Formatters.Add(new JsonOutputFormatter(settings, ArrayPool<char>.Shared));
}
}

Create an action filter that can be used on which ever action you want to add that functionality. I had achieved the same in Asp.Net Web API 2 (not core) using a DelegatingHandler that would inspect the request and update the json formatter's indent based on prettify=true query parameter in the URL
Here is how it was done with the delegating handler
/// <summary>
/// Custom handler to allow pretty print json results.
/// </summary>
public class PrettyPrintJsonHandler : DelegatingHandler {
const string prettyPrintConstant = "pretty";
MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
private System.Web.Http.HttpConfiguration httpConfig;
/// <summary>
/// Initializes a new instance of the <seealso cref="PrettyPrintJsonHandler"/> class with an HTTP Configuration.
/// </summary>
/// <param name="config"></param>
public PrettyPrintJsonHandler(System.Web.Http.HttpConfiguration config) {
this.httpConfig = config;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) {
var canPrettyPrint = checkQuery(request.RequestUri.Query);
var jsonFormatter = httpConfig.Formatters.JsonFormatter;
jsonFormatter.Indent = canPrettyPrint;
var response = await base.SendAsync(request, cancellationToken);
if (canPrettyPrint && response.Content != null) {
response.Content.Headers.ContentType = contentType;
}
return response;
}
private bool checkQuery(string queryString) {
var canPrettyPrint = false;
if (!string.IsNullOrWhiteSpace(queryString)) {
var prettyPrint = QueryString.Parse(queryString)[prettyPrintConstant];
canPrettyPrint = !string.IsNullOrWhiteSpace(prettyPrint) && Boolean.TryParse(prettyPrint, out canPrettyPrint) && canPrettyPrint;
}
return canPrettyPrint;
}
}
which was added as a global message handler during setup.
The same concept could be applied to an action filter.

Based on #Valentine's answer, and for Newtonsoft Json.NET (and .NET Core), I came up with this action filter implementation, which formats the response for content values except where the content value type is string:
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var objectResult = context.Result as ObjectResult;
if (objectResult?.Value == null) return;
if (objectResult.Value is string) return; // ignore string values - it would add extra quotes around the string in the response
var jsonFormatter = new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings
{
// set custom json settings here
},
ArrayPool<char>.Shared,
new MvcOptions(),
new MvcNewtonsoftJsonOptions()
);
objectResult.Formatters.Add(jsonFormatter);
}
}
Register the action filter in the Startup.ConfigureServices to apply it to all MVC/Web API controllers:
services.AddControllersWithViews(options =>
{
options.Filters.Add(new OutputFormatActionFilter());
});

Related

Is there anyway to modify request body before it gets bind to controller action method in asp.net web api 2

i am trying to send encrypted request to asp.net web api and want web api to intercept the request and decrypt or modify it before it gets bind to controller action method.
Any help will be appreciate.
What you could do is write your custom filter. Since you want to decode the body content before it gets bind, you will have to use AuthorizeAttribute.
public class DecryptRequestContent : System.Web.Http.AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var requestContent = actionContext.Request.Content;
var newContent = Decryption (requestContent);
actionContext.Request.Content = newContent;
}
}
After that, you will have to decorate your API with this filter like this:
public class SomeController : ApiController
{
[DecryptRequestContent]
public void SomeMethod(DataModel model)
{
// implementation goes here
}
}
I think the solution is
actionContext.Request.Content = new StringContent(newContent,Encoding.UTF8,"application/json");
Hopefully it can help!
Updated for .net core, authorization attribute that changes the body of the request
public class Authorize0ActionFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var requestContent = new StringContent("new body", Encoding.UTF8, "application/json");
var stream = requestContent.ReadAsStream();//modified stream
context.HttpContext.Request.Body = stream;
}
}
public class Authorize0Attribute : TypeFilterAttribute
{
public Authorize0Attribute()
: base(typeof(Authorize0ActionFilter))
{
//Arguments = new object[] { permission };
}
}

Is there any way to get request body in .NET Core FilterAttribute?

Sample of my request
http://localhost:8065/api/note
POST
content-type:application/json
request body: { "id" : "1234", "title" : "test", "status" : "draft"}
and the response should be
{ "msg" : "ok", "code" : 1}
The action
public async Task<IActionResult> Post([FromBody]NoteModel model)
In order to have every request logged automatically, I create an attribute to do this job. The attribute looks like: (from Microsoft Docs)
public class SampleActionFilterAttribute : TypeFilterAttribute
{
public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
{
}
private class SampleActionFilterImpl : IActionFilter
{
private readonly ILogger _logger;
public SampleActionFilterImpl(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogDebug("[path]" + context.HttpContext.Request.Path);
_logger.LogDebug("[method]" + context.HttpContext.Request.Method);
_logger.LogDebug("[body]"); //log request body, expectation: { "id" : "1234", "title" : "test", "status" : "draft"}
_logger.LogDebug("[statuscode]" + context.HttpContext.Response.StatusCode);
_logger.LogDebug("[response]"); //log response
}
}
}
I try to use streamReader to get request body only get empty string.
StreamReader reader = new StreamReader(context.HttpContext.Request.Body);
string text = reader.ReadToEnd();
Is that because the body was read by [fromBody] from controller so the stream can not be read twice? If so, how am I supposed to get request body and response in OnActionExecuted method?
Update:
I've just copied Set's code into my project, not working. Here is the debug gif
Accordingly to this "Best way to log/read request body in a middleware" thread, the following should work:
// using Microsoft.AspNetCore.Http.Internal;
public class SampleActionFilterAttribute : TypeFilterAttribute
{
...
public void OnActionExecuting(ActionExecutedContext context)
{
// read body before MVC action execution
string bodyData = ReadBodyAsString(context.HttpContext.Request);
}
private string ReadBodyAsString(HttpRequest request)
{
var initialBody = request.Body; // Workaround
try
{
request.EnableRewind();
using (StreamReader reader = new StreamReader(request.Body))
{
string text = reader.ReadToEnd();
return text;
}
}
finally
{
// Workaround so MVC action will be able to read body as well
request.Body = initialBody;
}
return string.Empty;
}
}
Also similar approach described in Read request body twice SO post
Update: above approach in ReadBodyAsString with will work if used in middleware, not in action filter. The difference is that when action filter is calling (even for OnActionExecuting), the body stream already has been read and [FromBody] model has been populated.
The good nesw is that so it is possible to get model directly in action filter by using context.ActionArguments["<model_name>"]. In your case:
public void OnActionExecuted(ActionExecutedContext context)
{
var model = context.ActionArguments["model"] as NoteModel;
}
Following snippet worked for me, to log request only if there is any exception.(.Net Core 3.1)
{
public class ExceptionFilter : IActionFilter
{
private ConcurrentDictionary<string, string> requests = new ConcurrentDictionary<string, string>();
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception != null)
{
StringBuilder parameters = new StringBuilder();
_logger.LogError("Error while executing action:" + context.ActionDescriptor.DisplayName);
string errRequest;
if(requests.TryGetValue(context.HttpContext.TraceIdentifier,out errRequest))
{
_logger.LogError(errRequest);
}
_logger.LogError(context.Exception);
context.Result = new ObjectResult("Error!!!")
{
StatusCode = 500,
};
context.ExceptionHandled = true;
}
string req;
requests.Remove(context.HttpContext.TraceIdentifier, out req);
}
public void OnActionExecuting(ActionExecutingContext context)
{
StringBuilder sb = new StringBuilder();
foreach (var arg in context.ActionArguments)
{
sb.Append(arg.Key.ToString() + ":" + Newtonsoft.Json.JsonConvert.SerializeObject(arg.Value) + "\n");
}
requests.TryAdd(context.HttpContext.TraceIdentifier, sb.ToString());
}
}
}
in .net core 3.1, I usually use this approach to handle this scenario
1- Create Async Filter Attribute
public class MyFilter : IAsyncAuthorizationFilter
{
private string _errorMessage = UserAccessErrorMessages.NO_ACCESS;
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
string requestBody = await ReadBodyAsString(context.HttpContext.Request);
var reportFiltration = JsonConvert.DeserializeObject<YourModel>(requestBody);
var _myService = (IMyService)context.HttpContext.RequestServices.GetService(typeof(IMyService));
if (!hasAccess)
{
context.Result = new UnauthorizedObjectResult(_errorMessage);
}
}
private async Task<string> ReadBodyAsString(Microsoft.AspNetCore.Http.HttpRequest request)
{
var initialBody = request.Body; // Workaround
try
{
//request.EnableRewind();
using (StreamReader reader = new StreamReader(request.Body))
{
string text = await reader.ReadToEndAsync();
return text;
}
}
finally
{
// Workaround so MVC action will be able to read body as well
request.Body = initialBody;
}
}
}
2- Create Your Custom Attribute
public class MyAttribute : TypeFilterAttribute, IAllowAnonymous
{
public MyAttribute () : base(typeof(MyFilter))
{
}
}
3- use your filter
[HttpPost]
[ActionName("MyAction")]
[MyAttribute]
public async Task<IActionResult> PostData([FromBody]MyModel model)
If you're using IActionResult in your controllers and you want the .NET objects, you can write a filter like this:
public class SampleFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult)
{
var objResult = (ObjectResult)context.Result;
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
By the point it hits OnActionExecuted, the ObjectResult task has already completed, so you can just extract the value. You can also get the StatusCode with objResult.StatusCode.
In the controller, return Ok(...) actually creates an OkObjectResult, etc.
If you specifically want the serialzied result, then Set's answer is more valid.

View POST request body in Application Insights

Is it possible to view POST request body in Application Insights?
I can see request details, but not the payload being posted in application insights. Do I have to track this with some coding?
I am building a MVC core 1.1 Web Api.
You can simply implement your own Telemetry Initializer:
For example, below an implementation that extracts the payload and adds it as a custom dimension of the request telemetry:
public class RequestBodyInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry != null && (requestTelemetry.HttpMethod == HttpMethod.Post.ToString() || requestTelemetry.HttpMethod == HttpMethod.Put.ToString()))
{
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
string requestBody = reader.ReadToEnd();
requestTelemetry.Properties.Add("body", requestBody);
}
}
}
}
Then add it to the configuration either by configuration file or via code:
TelemetryConfiguration.Active.TelemetryInitializers.Add(new RequestBodyInitializer());
Then query it in Analytics:
requests | limit 1 | project customDimensions.body
The solution provided by #yonisha is in my opinion the cleanest one available. However you still need to get your HttpContext in there and for that you need some more code. I have also inserted some comments which are based or taken from code examples above. It is important to reset the position of your request else you will lose its data.
This is my solution that I have tested and gives me the jsonbody:
public class RequestBodyInitializer : ITelemetryInitializer
{
readonly IHttpContextAccessor httpContextAccessor;
public RequestBodyInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
if (telemetry is RequestTelemetry requestTelemetry)
{
if ((httpContextAccessor.HttpContext.Request.Method == HttpMethods.Post ||
httpContextAccessor.HttpContext.Request.Method == HttpMethods.Put) &&
httpContextAccessor.HttpContext.Request.Body.CanRead)
{
const string jsonBody = "JsonBody";
if (requestTelemetry.Properties.ContainsKey(jsonBody))
{
return;
}
//Allows re-usage of the stream
httpContextAccessor.HttpContext.Request.EnableRewind();
var stream = new StreamReader(httpContextAccessor.HttpContext.Request.Body);
var body = stream.ReadToEnd();
//Reset the stream so data is not lost
httpContextAccessor.HttpContext.Request.Body.Position = 0;
requestTelemetry.Properties.Add(jsonBody, body);
}
}
}
Then also be sure to add this to your Startup -> ConfigureServices
services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();
EDIT:
If you also want to get the response body I found it useful to create a piece of middleware (.NET Core, not sure about Framework). At first I took above approach where you log a response and a request but most of the time you want these together:
public async Task Invoke(HttpContext context)
{
var reqBody = await this.GetRequestBodyForTelemetry(context.Request);
var respBody = await this.GetResponseBodyForTelemetry(context);
this.SendDataToTelemetryLog(reqBody, respBody, context);
}
This awaits both a request and a response. GetRequestBodyForTelemetry is almost identical to the code from the telemetry initializer, except using Task. For the response body I have used the code below, I also excluded a 204 since that leads to a nullref:
public async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
var originalBody = context.Response.Body;
try
{
using (var memStream = new MemoryStream())
{
context.Response.Body = memStream;
//await the responsebody
await next(context);
if (context.Response.StatusCode == 204)
{
return null;
}
memStream.Position = 0;
var responseBody = new StreamReader(memStream).ReadToEnd();
//make sure to reset the position so the actual body is still available for the client
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
return responseBody;
}
}
finally
{
context.Response.Body = originalBody;
}
}
Few days back, I got a similar requirement to log the request Body in Application insights with filtering out sensitive input user data from the payload. So sharing my solution. The below solution is developed for ASP.NET Core 2.0 Web API.
ActionFilterAttribute
I've used ActionFilterAttribute from (Microsoft.AspNetCore.Mvc.Filters namespace) which provides the Model via ActionArgument so that by reflection, those properties can be extracted which are marked as sensitive.
public class LogActionFilterAttribute : ActionFilterAttribute
{
private readonly IHttpContextAccessor httpContextAccessor;
public LogActionFilterAttribute(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext.Request.Method == HttpMethods.Post || context.HttpContext.Request.Method == HttpMethods.Put)
{
// Check parameter those are marked for not to log.
var methodInfo = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor).MethodInfo;
var noLogParameters = methodInfo.GetParameters().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(p => p.Name);
StringBuilder logBuilder = new StringBuilder();
foreach (var argument in context.ActionArguments.Where(a => !noLogParameters.Contains(a.Key)))
{
var serializedModel = JsonConvert.SerializeObject(argument.Value, new JsonSerializerSettings() { ContractResolver = new NoPIILogContractResolver() });
logBuilder.AppendLine($"key: {argument.Key}; value : {serializedModel}");
}
var telemetry = this.httpContextAccessor.HttpContext.Items["Telemetry"] as Microsoft.ApplicationInsights.DataContracts.RequestTelemetry;
if (telemetry != null)
{
telemetry.Context.GlobalProperties.Add("jsonBody", logBuilder.ToString());
}
}
await next();
}
}
The 'LogActionFilterAttribute' is injected in MVC pipeline as Filter.
services.AddMvc(options =>
{
options.Filters.Add<LogActionFilterAttribute>();
});
NoLogAttribute
In above code, NoLogAttribute attribute is used which should be applied on Model/Model's Properties or method parameter to indicate that value should not be logged.
public class NoLogAttribute : Attribute
{
}
NoPIILogContractResolver
Also, NoPIILogContractResolver is used in JsonSerializerSettings during serialization process
internal class NoPIILogContractResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = new List<JsonProperty>();
if (!type.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute)))
{
IList<JsonProperty> retval = base.CreateProperties(type, memberSerialization);
var excludedProperties = type.GetProperties().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(s => s.Name);
foreach (var property in retval)
{
if (excludedProperties.Contains(property.PropertyName))
{
property.PropertyType = typeof(string);
property.ValueProvider = new PIIValueProvider("PII Data");
}
properties.Add(property);
}
}
return properties;
}
}
internal class PIIValueProvider : IValueProvider
{
private object defaultValue;
public PIIValueProvider(string defaultValue)
{
this.defaultValue = defaultValue;
}
public object GetValue(object target)
{
return this.defaultValue;
}
public void SetValue(object target, object value)
{
}
}
PIITelemetryInitializer
To inject the RequestTelemetry object, I've to use ITelemetryInitializer so that RequestTelemetry can be retrieved in LogActionFilterAttribute class.
public class PIITelemetryInitializer : ITelemetryInitializer
{
IHttpContextAccessor httpContextAccessor;
public PIITelemetryInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
if (this.httpContextAccessor.HttpContext != null)
{
if (telemetry is Microsoft.ApplicationInsights.DataContracts.RequestTelemetry)
{
this.httpContextAccessor.HttpContext.Items.TryAdd("Telemetry", telemetry);
}
}
}
}
The PIITelemetryInitializer is registered as
services.AddSingleton<ITelemetryInitializer, PIITelemetryInitializer>();
Testing feature
Following code demonstrates the usage of above code
Created a controller
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly ILogger _logger;
public ValuesController(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<ValuesController>();
}
// POST api/values
[HttpPost]
public void Post([FromBody, NoLog]string value)
{
}
[HttpPost]
[Route("user")]
public void AddUser(string id, [FromBody]User user)
{
}
}
Where User Model is defined as
public class User
{
[NoLog]
public string Id { get; set; }
public string Name { get; set; }
public DateTime AnneviseryDate { get; set; }
[NoLog]
public int LinkId { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public string AddressLine { get; set; }
[NoLog]
public string City { get; set; }
[NoLog]
public string Country { get; set; }
}
So when API is invoked by Swagger tool
The jsonBody is logged in Request without sensitive data. All sensitive data is replaced by 'PII Data' string literal.
Update: I have put the logic below into a ready-to-use NuGet package. You can find more about the package here and about the topic itself here.
I choose the custom middleware path as it made things easier with HttpContext already being there.
public class RequestBodyLoggingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var method = context.Request.Method;
// Ensure the request body can be read multiple times
context.Request.EnableBuffering();
// Only if we are dealing with POST or PUT, GET and others shouldn't have a body
if (context.Request.Body.CanRead && (method == HttpMethods.Post || method == HttpMethods.Put))
{
// Leave stream open so next middleware can read it
using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
bufferSize: 512, leaveOpen: true);
var requestBody = await reader.ReadToEndAsync();
// Reset stream position, so next middleware can read it
context.Request.Body.Position = 0;
// Write request body to App Insights
var requestTelemetry = context.Features.Get<RequestTelemetry>();
requestTelemetry?.Properties.Add("RequestBody", requestBody);
}
// Call next middleware in the pipeline
await next(context);
}
}
And this is how I log the response body
public class ResponseBodyLoggingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var originalBodyStream = context.Response.Body;
try
{
// Swap out stream with one that is buffered and suports seeking
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
// hand over to the next middleware and wait for the call to return
await next(context);
// Read response body from memory stream
memoryStream.Position = 0;
var reader = new StreamReader(memoryStream);
var responseBody = await reader.ReadToEndAsync();
// Copy body back to so its available to the user agent
memoryStream.Position = 0;
await memoryStream.CopyToAsync(originalBodyStream);
// Write response body to App Insights
var requestTelemetry = context.Features.Get<RequestTelemetry>();
requestTelemetry?.Properties.Add("ResponseBody", responseBody);
}
finally
{
context.Response.Body = originalBodyStream;
}
}
}
Than add an extension method...
public static class ApplicationInsightExtensions
{
public static IApplicationBuilder UseRequestBodyLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestBodyLoggingMiddleware>();
}
public static IApplicationBuilder UseResponseBodyLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseBodyLoggingMiddleware>();
}
}
...that allows for a clean integration inside Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// Enable our custom middleware
app.UseRequestBodyLogging();
app.UseResponseBodyLogging();
}
// ...
}
Don't forget to register the custom middleware components inside ConfigureServices()
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
services.AddTransient<RequestBodyLoggingMiddleware>();
services.AddTransient<ResponseBodyLoggingMiddleware>();
}
I never got #yonisha's answer working so I used a DelegatingHandler instead:
public class MessageTracingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Trace the request
await TraceRequest(request);
// Execute the request
var response = await base.SendAsync(request, cancellationToken);
// Trace the response
await TraceResponse(response);
return response;
}
private async Task TraceRequest(HttpRequestMessage request)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var requestTraceInfo = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
var body = requestTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Request Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
private async Task TraceResponse(HttpResponseMessage response)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var responseTraceInfo = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : null;
var body = responseTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Response Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
}
.GetRequestTelemetry() is an extension method from Microsoft.ApplicationInsights.Web.
In Asp.Net core it looks like we dont have to use ITelemetryInitializer. We can use the middleware to log the requests to application insights. Thanks to #IanKemp https://github.com/microsoft/ApplicationInsights-aspnetcore/issues/686
public async Task Invoke(HttpContext httpContext)
{
var requestTelemetry = httpContext.Features.Get<RequestTelemetry>();
//Handle Request
var request = httpContext.Request;
if (request?.Body?.CanRead == true)
{
request.EnableBuffering();
var bodySize = (int)(request.ContentLength ?? request.Body.Length);
if (bodySize > 0)
{
request.Body.Position = 0;
byte[] body;
using (var ms = new MemoryStream(bodySize))
{
await request.Body.CopyToAsync(ms);
body = ms.ToArray();
}
request.Body.Position = 0;
if (requestTelemetry != null)
{
var requestBodyString = Encoding.UTF8.GetString(body);
requestTelemetry.Properties.Add("RequestBody", requestBodyString);
}
}
}
await _next(httpContext); // calling next middleware
}
I implemented a middleware for this,
Invoke method does,
if (context.Request.Method == "POST" || context.Request.Method == "PUT")
{
var bodyStr = GetRequestBody(context);
var telemetryClient = new TelemetryClient();
var traceTelemetry = new TraceTelemetry
{
Message = bodyStr,
SeverityLevel = SeverityLevel.Verbose
};
//Send a trace message for display in Diagnostic Search.
telemetryClient.TrackTrace(traceTelemetry);
}
Where, GetRequestBody is like,
private static string GetRequestBody(HttpContext context)
{
var bodyStr = "";
var req = context.Request;
//Allows using several time the stream in ASP.Net Core.
req.EnableRewind();
//Important: keep stream opened to read when handling the request.
using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
{
bodyStr = reader.ReadToEnd();
}
// Rewind, so the core is not lost when it looks the body for the request.
req.Body.Position = 0;
return bodyStr;
}
I can able to log the request message body in Application Insights using #yonisha method but I can't able to log the response message body. I am interested in logging the response message body. I am already logging the Post, Put, Delete Request message body using #yonisha method.
When I tried to access the response body in the TelemetryInitializer I keep getting an exception with an error message saying that "stream was not readable. When I researched more I found that AzureInitializer is running as part of HttpModule(ApplicationInsightsWebTracking) so by the time it gets control response object is disposed.
I got an idea from #Oskar answer. Why not have a delegate handler and record the response since the response object is not disposed at the stage of message handler. The message handler is part of the Web API life cycle i.e. similar to the HTTP module but confined to web API. When I developed and tested this idea, fortunately, It worked I recorded the response in the request message using message handler and retrieved it at the AzureInitializer (HTTP module whose execution happens later than the message handler). Here is the sample code.
public class AzureRequestResponseInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry != null && HttpContext.Current != null && HttpContext.Current.Request != null)
{
if ((HttpContext.Current.Request.HttpMethod == HttpMethod.Post.ToString()
|| HttpContext.Current.Request.HttpMethod == HttpMethod.Put.ToString()) &&
HttpContext.Current.Request.Url.AbsoluteUri.Contains("api"))
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
HttpContext.Current.Request.InputStream.Position = 0;
string requestBody = reader.ReadToEnd();
if (requestTelemetry.Properties.Keys.Contains("requestbody"))
{
requestTelemetry.Properties["requestbody"] = requestBody;
}
else
{
requestTelemetry.Properties.Add("requestbody", requestBody);
}
}
else if (HttpContext.Current.Request.HttpMethod == HttpMethod.Get.ToString()
&& HttpContext.Current.Response.ContentType.Contains("application/json"))
{
var netHttpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
if (netHttpRequestMessage.Properties.Keys.Contains("responsejson"))
{
var responseJson = netHttpRequestMessage.Properties["responsejson"].ToString();
if (requestTelemetry.Properties.Keys.Contains("responsebody"))
{
requestTelemetry.Properties["responsebody"] = responseJson;
}
else
{
requestTelemetry.Properties.Add("responsebody", responseJson);
}
}
}
}
}
}
config.MessageHandlers.Add(new LoggingHandler());
public class LoggingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith(task =>
{
var response = task.Result;
StoreResponse(response);
return response;
});
}
private void StoreResponse(HttpResponseMessage response)
{
var request = response.RequestMessage;
(response.Content ?? new StringContent("")).ReadAsStringAsync().ContinueWith(x =>
{
var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;
if (request.Properties.ContainsKey("responseJson"))
{
request.Properties["responsejson"] = x.Result;
}
else
{
request.Properties.Add("responsejson", x.Result);
}
});
}
}
The solution provided by yonisha is clean, but it does not work for me in .Net Core 2.0. This works if you have a JSON body:
public IActionResult MyAction ([FromBody] PayloadObject payloadObject)
{
//create a dictionary to store the json string
var customDataDict = new Dictionary<string, string>();
//convert the object to a json string
string activationRequestJson = JsonConvert.SerializeObject(
new
{
payloadObject = payloadObject
});
customDataDict.Add("body", activationRequestJson);
//Track this event, with the json string, in Application Insights
telemetryClient.TrackEvent("MyAction", customDataDict);
return Ok();
}
I am sorry, #yonisha's solution does not seem to work in .NET 4.7. The Application Insights part works OK, but there is actually no simple way to get the request body inside the telemetry initializer in .NET 4.7. .NET 4.7 uses GetBufferlessInputStream() to get the stream, and this stream is "read once". One potential code is like this:
private static void LogRequestBody(ISupportProperties requestTelemetry)
{
var requestStream = HttpContext.Current?.Request?.GetBufferlessInputStream();
if (requestStream?.Length > 0)
using (var reader = new StreamReader(requestStream))
{
string body = reader.ReadToEnd();
requestTelemetry.Properties["body"] = body.Substring(0, Math.Min(body.Length, 8192));
}
}
But the return from GetBufferlessInputStream() is already consumed, and does not support seeking. Therefore, the body will always be an empty string.

Need to log asp.net webapi 2 request and response body to a database

I am using Microsoft Asp.net WebApi2 hosted on IIS. I very simply would like to log the request body (XML or JSON) and the response body for each post.
There is nothing special about this project or the controller processing the post. I am not interested in using logging frameworks like nLog, elmah, log4net, or the built-in tracing features of web API unless it is necessary to do so.
I am simply wanting to know where to put my logging code and how to get the actual JSON or XML from the incoming and outgoing request and response.
My controller post method:
public HttpResponseMessage Post([FromBody])Employee employee)
{
if (ModelState.IsValid)
{
// insert employee into to the database
}
}
I would recommend using a DelegatingHandler. Then you will not need to worry about any logging code in your controllers.
public class LogRequestAndResponseHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Content != null)
{
// log request body
string requestBody = await request.Content.ReadAsStringAsync();
Trace.WriteLine(requestBody);
}
// let other handlers process the request
var result = await base.SendAsync(request, cancellationToken);
if (result.Content != null)
{
// once response body is ready, log it
var responseBody = await result.Content.ReadAsStringAsync();
Trace.WriteLine(responseBody);
}
return result;
}
}
Just replace Trace.WriteLine with your logging code and register the handler in WebApiConfig like this:
config.MessageHandlers.Add(new LogRequestAndResponseHandler());
Here is the full Microsoft documentation for Message Handlers.
There are multiple approaches to generically handle Request/Response logging for every WebAPI method calls:
ActionFilterAttribute:
One can write custom ActionFilterAttribute and decorate the controller/action methods to enable logging.
Con: You need to decorate every controller/methods (still you can do it on base controller, but still it doesn't address cross cutting concerns.
Override BaseController and handle logging there.
Con: We are expecting/forcing the controllers to inherit from a custom base controller.
Using DelegatingHandler.
Advantage: We are not touching controller/method here with this approach. Delegating handler sits in isolation and gracefully handles the request/response logging.
For more indepth article, refer this http://weblogs.asp.net/fredriknormen/log-message-request-and-response-in-asp-net-webapi.
One of the option you have is using creating a action filter and decorating your WebApiController/ApiMethod with it.
Filter Attribute
public class MyFilterAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.Request.Method == HttpMethod.Post)
{
var postData = actionContext.ActionArguments;
//do logging here
}
}
}
WebApi controller
[MyFilterAttribute]
public class ValuesController : ApiController{..}
or
[MyFilterAttribute]
public void Post([FromBody]string value){..}
Hope this helps.
Getting access to request message is easy. Your base class, ApiController contains .Request property, which, as name suggests, contains the request in parsed form. You simply examine it for whatever you're looking to log and pass it to your logging facility, whichever it may be. This code you can put in the beginning of your action, if you need to do it for just one or a handful.
If you need to do it on all actions (all meaning more than a manageable handful), then what you can do is override .ExecuteAsync method to capture every action call for your controller.
public override Task<HttpResponseMessage> ExecuteAsync(
HttpControllerContext controllerContext,
CancellationToken cancellationToken
)
{
// Do logging here using controllerContext.Request
return base.ExecuteAsync(controllerContext, cancellationToken);
}
This seems to be a pretty old thread but worh sharing another solution.
You can add this method in your global.asax file which will be triggered every after HTTP request ends.
void Application_EndRequest(Object Sender, EventArgs e)
{
var request = (Sender as HttpApplication).Request;
var response = (Sender as HttpApplication).Response;
if (request.HttpMethod == "POST" || request.HttpMethod == "PUT")
{
byte[] bytes = request.BinaryRead(request.TotalBytes);
string body = Encoding.UTF7.GetString(bytes);
if (!String.IsNullOrEmpty(body))
{
// Do your logic here (Save in DB, Log in IIS etc.)
}
}
}
This is really old topic but I spent much time(search the internet) to do these thing so I will just post my solution here.
Concept
Override ExecuteAsync of APicontroller method for tracking Inbound request,in my solution I create Base_ApiController as a parent of my project's API controllers .
Use System.Web.Http.Filters.ActionFilterAttribute to track Outbound response of api controller
***(Additional)***Use System.Web.Http.Filters.ExceptionFilterAttribute to log when exception occure.
1. MyController.cs
[APIExceptionFilter] // use 3.
[APIActionFilter] // use 2.
public class Base_APIController : ApiController
{
public bool IsLogInbound
{
get
{ return ConfigurationManager.AppSettings["LogInboundRequest"] =="Y"? true:false ; }
}
/// <summary>
/// for logging exception
/// </summary>
/// <param name="controllerContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override Task<HttpResponseMessage> ExecuteAsync(
HttpControllerContext controllerContext,
CancellationToken cancellationToken
)
{
// Do logging here using controllerContext.Request
// I don't know why calling the code below make content not null Kanit P.
var content = controllerContext.Request.Content.ReadAsStringAsync().Result.ToString(); // keep request json content
// Do your own logging!
if (IsLogInbound)
{
try
{
ErrLog.Insert(ErrLog.type.InboundRequest, controllerContext.Request,
controllerContext.Request.RequestUri.AbsoluteUri
, content);
}
catch (Exception e) { }
}
// will not log err when go to wrong controller's action (error here but not go to APIExceptionFilter)
var t = base.ExecuteAsync(controllerContext, cancellationToken);
if (!t.Result.IsSuccessStatusCode)
{
}
return t;
}
2. APIActionFilter.cs
public class APIActionFilter : System.Web.Http.Filters.ActionFilterAttribute
{
public bool LogOutboundRequest
{
get
{ return ConfigurationManager.AppSettings["LogInboundRequest"] == "Y" ? true : false; }
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try {
var returndata = actionExecutedContext.Response.Content.ReadAsStringAsync().Result.ToString();
//keep Json response content
// Do your own logging!
if (LogOutboundRequest)
{
ErrLog.Insert(ErrLog.type.OutboundResponse, actionExecutedContext.Response.Headers,
actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName
+ "/"
+ actionExecutedContext.ActionContext.ActionDescriptor.ActionName
, returndata );
}
} catch (Exception e) {
}
}
}
}
3. APIExceptionFilter.cs
public class APIExceptionFilter : ExceptionFilterAttribute
{
public bool IsLogErr
{
get
{ return ConfigurationManager.AppSettings["LogExceptionRequest"] == "Y" ? true : false; }
}
public override void OnException(HttpActionExecutedContext context)
{
try
{
//Do your own logging!
if (IsLogErr)
{
ErrLog.Insert(ErrLog.type.APIFilterException, context.Request,
context.ActionContext.ControllerContext.ControllerDescriptor.ControllerName
+ "/"
+ context.ActionContext.ActionDescriptor.ActionName
, context.Exception.ToString() + context.Exception.StackTrace);
}
}catch(Exception e){
}
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
else {
context.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
}

Return error on invalid or expired token

I'm trying to implement OAuth Bearer Authentication with Owin. When an invalid or expired token is passed, the default implementation is to log this as a warning and just don't set an Identity. I however would like to reject the whole request with an error in this case. But how would I do this?
After digging through the code I found out that in OAuthBearerAuthenticationHandler it will parse the token using a fallback mechanism when the provided AuthenticationTokenProvider did not parse any ticket (like the default implementation). This handler will log a warning when the token could not be parsed to any ticket or when it expired.
But I can't find any place to plug in my own logic to what happens when the token is invalid or expired. I could theoretically check this on my own in the AuthenticationTokenProvider, but then I would have to reimplement the logic (= copy it over) for creating and reading the token. Also this seems just out of place, as this class seems to be only responsible for creating and parsing tokens. I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.
What do I overlook? How would I go on about this the best?
edit:
For clarification. I know by not setting an identity the request will be rejected with 401 Unauthorized later in the Web API. But I personally see this as really bad style, silently swallowing an erroneous access token without any notification. This way you don't get to know that your token is crap, you just get to know you're not authorized.
I had a similar issue, i think the answer is to late but someone will come here with a similar problem:
I used this nuget package for validate authentication, but i think any method can help: https://www.nuget.org/packages/WebApi.AuthenticationFilter. You can read its documentation in this site https://github.com/mbenford/WebApi-AuthenticationFilter
AuthenticationFilter.cs
public class AuthenticationFilter : AuthenticationFilterAttribute{
public override void OnAuthentication(HttpAuthenticationContext context)
{
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
var ci = context.Principal.Identity as ClaimsIdentity;
//First of all we are going to check that the request has the required Authorization header. If not set the Error
var authHeader = context.Request.Headers.Authorization;
//Change "Bearer" for the needed schema
if (authHeader == null || authHeader.Scheme != "Bearer")
{
context.ErrorResult = context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "Request require authorization" } });
}
//If the token has expired the property "IsAuthenticated" would be False, then set the error
else if (!ci.IsAuthenticated)
{
context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "The Token has expired" } });
}
}}
AuthenticationFailureResult.cs
public class AuthenticationFailureResult : IHttpActionResult{
private object ResponseMessage;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request, object responseMessage)
{
ReasonPhrase = reasonPhrase;
Request = request;
ResponseMessage = responseMessage;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
response.Content = new System.Net.Http.ObjectContent<object>(ResponseMessage, jsonFormatter);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}}
Response examples:
{"Error":{"Code":401,"Message":"Request require authorization"}}
{"Error":{"Code":401,"Message":"The Token has expired"}}
Fonts and inspiration documentation:
//github.com/mbenford/WebApi-AuthenticationFilter
//www.asp.net/web-api/overview/security/authentication-filters
Yeah, I did not find 'good' solution for this,
I also don't see a way to plug in my own implementation of the
OAuthBearerAuthenticationHandler in the
OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole
middleware, but this also seems very overkill.
Agreed, but that's what I did (before reading your post). I copy & pasted three owin classes, and made it so that it sets property in Owins context, which can be later checked by other handlers.
public static class OAuthBearerAuthenticationExtensions
{
public static IAppBuilder UseOAuthBearerAuthenticationExtended(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
app.Use(typeof(OAuthBearerAuthenticationMiddlewareExtended), app, options);
app.UseStageMarker(PipelineStage.Authenticate);
return app;
}
}
internal class OAuthBearerAuthenticationHandlerExtended : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
public OAuthBearerAuthenticationHandlerExtended(ILogger logger, string challenge)
{
_logger = logger;
_challenge = challenge;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
// Give application opportunity to find from a different location, adjust, or reject token
var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken);
await Options.Provider.RequestToken(requestTokenContext);
// If no token found, no further work possible
if (string.IsNullOrEmpty(requestTokenContext.Token))
{
return null;
}
// Call provider to process the token into data
var tokenReceiveContext = new AuthenticationTokenReceiveContext(
Context,
Options.AccessTokenFormat,
requestTokenContext.Token);
await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
if (tokenReceiveContext.Ticket == null)
{
tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
}
AuthenticationTicket ticket = tokenReceiveContext.Ticket;
if (ticket == null)
{
_logger.WriteWarning("invalid bearer token received");
Context.Set("oauth.token_invalid", true);
return null;
}
// Validate expiration time if present
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
if (ticket.Properties.ExpiresUtc.HasValue &&
ticket.Properties.ExpiresUtc.Value < currentUtc)
{
_logger.WriteWarning("expired bearer token received");
Context.Set("oauth.token_expired", true);
return null;
}
// Give application final opportunity to override results
var context = new OAuthValidateIdentityContext(Context, Options, ticket);
if (ticket != null &&
ticket.Identity != null &&
ticket.Identity.IsAuthenticated)
{
// bearer token with identity starts validated
context.Validated();
}
if (Options.Provider != null)
{
await Options.Provider.ValidateIdentity(context);
}
if (!context.IsValidated)
{
return null;
}
// resulting identity values go back to caller
return context.Ticket;
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return null;
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode != 401)
{
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge);
Options.Provider.ApplyChallenge(challengeContext);
}
return Task.FromResult<object>(null);
}
}
public class OAuthBearerAuthenticationMiddlewareExtended : AuthenticationMiddleware<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
/// <summary>
/// Bearer authentication component which is added to an OWIN pipeline. This constructor is not
/// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication
/// extension method.
///
/// </summary>
public OAuthBearerAuthenticationMiddlewareExtended(OwinMiddleware next, IAppBuilder app, OAuthBearerAuthenticationOptions options)
: base(next, options)
{
_logger = AppBuilderLoggerExtensions.CreateLogger<OAuthBearerAuthenticationMiddlewareExtended>(app);
_challenge = string.IsNullOrWhiteSpace(Options.Challenge) ? (!string.IsNullOrWhiteSpace(Options.Realm) ? "Bearer realm=\"" + this.Options.Realm + "\"" : "Bearer") : this.Options.Challenge;
if (Options.Provider == null)
Options.Provider = new OAuthBearerAuthenticationProvider();
if (Options.AccessTokenFormat == null)
Options.AccessTokenFormat = new TicketDataFormat(
Microsoft.Owin.Security.DataProtection.AppBuilderExtensions.CreateDataProtector(app, typeof(OAuthBearerAuthenticationMiddleware).Namespace, "Access_Token", "v1"));
if (Options.AccessTokenProvider != null)
return;
Options.AccessTokenProvider = new AuthenticationTokenProvider();
}
/// <summary>
/// Called by the AuthenticationMiddleware base class to create a per-request handler.
///
/// </summary>
///
/// <returns>
/// A new instance of the request handler
/// </returns>
protected override AuthenticationHandler<OAuthBearerAuthenticationOptions> CreateHandler()
{
return new OAuthBearerAuthenticationHandlerExtended(_logger, _challenge);
}
}
Then I wrote my own authorization filter, which will be applied globally:
public class AuthorizeAttributeExtended : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var tokenHasExpired = false;
var owinContext = OwinHttpRequestMessageExtensions.GetOwinContext(actionContext.Request);
if (owinContext != null)
{
tokenHasExpired = owinContext.Environment.ContainsKey("oauth.token_expired");
}
if (tokenHasExpired)
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_token",
error_message = "The Token has expired"
});
}
else
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_request",
error_message = "The Token is invalid"
});
}
}
}
public class AuthenticationFailureMessage : HttpResponseMessage
{
public AuthenticationFailureMessage(string reasonPhrase, HttpRequestMessage request, object responseMessage)
: base(HttpStatusCode.Unauthorized)
{
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
Content = new ObjectContent<object>(responseMessage, jsonFormatter);
RequestMessage = request;
ReasonPhrase = reasonPhrase;
}
}
my WebApiConfig:
config.Filters.Add(new AuthorizeAttributeExtended());
How my configureOAuth looks like:
public void ConfigureOAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions()
{
};
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AuthenticationMode = AuthenticationMode.Active
};
FacebookAuthOptions = new CustomFacebookAuthenticationOptions();
app.UseFacebookAuthentication(FacebookAuthOptions);
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthenticationExtended(OAuthBearerOptions);
}
I will try & get this to main branch of oAuth middleware, it seems like an obvious use case, unless I am missing something.
I came across this problem recently. We wanted to return a JSON message if the user's access token had expired, allowing the consumer web application to silently refresh the access token and re-issue the API request. We also didn't want to rely on the exceptions thrown for token lifetime validation.
Not wanting to re-implement any middleware, we specified the Provider option inside JwtBearerAuthenticationOptions and added a delegate to handle the OnRequestTokenMethod. The delegate checks to see if it can read the token passed to the middleware and sets a boolean inside the OWIN context if it's expired.
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = tokenValidationParameters,
Provider = new OAuthBearerAuthenticationProvider
{
OnRequestToken = (ctx) =>
{
if (!string.IsNullOrEmpty(ctx.Token))
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (handler.CanReadToken(ctx.Token))
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(ctx.Token);
if (jwtToken.IsExpired())
ctx.OwinContext.Set<bool>("expiredToken", true);
}
}
return Task.CompletedTask;
}
}
});
For convenience I added a quick extension method to check if a JWT expired:
public static class JwtSecurityTokenExtensions
{
public static bool IsExpired (this JwtSecurityToken token)
{
if (DateTime.UtcNow > token.ValidTo.ToUniversalTime())
return true;
return false;
}
}
We ended up using a middleware to check on the state of that boolean:
app.Use((context, next) =>
{
bool expiredToken = context.Get<bool>("expiredToken");
if (expiredToken)
{
// do stuff
}
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
Not exactly the most efficient code, since we're parsing the token again after the middleware already did and also introducing a new middleware to act on the result of the check, but it's a fresh perspective nonetheless.
If authentication fails (meaning the token is expired) then that layer doesn't set the user, as you said. It's up the the authorization layer (later on) to reject the call. So for your scenario your Web API would need to deny access to an anonymous caller. Use the [Authorize] authorization filter attribute.

Categories