System.ArgumentException HResult=0x80070057 Message=Stream is now writable Parameter name: stream - c#

I have asp.net core 2 web api endpoint along with custom middleware. In order to accomplish the centralized exception handling and request validation for the project I referred the link: https://www.strathweb.com/2018/07/centralized-exception-handling-and-request-validation-in-asp-net-core/
Based on the reference I implemented the following code :
// Startup
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseGraphiQl();
app.UseHttpsRedirection();
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = errorFeature.Error;
// the IsTrusted() extension method doesn't exist and
// you should implement your own as you may want to interpret it differently
// i.e. based on the current principal
var problemDetails = new ProblemDetails{Instance = $"urn:myorganization:error:{Guid.NewGuid()}"};
if (exception is BadHttpRequestException badHttpRequestException)
{
problemDetails.Title = "Invalid request";
problemDetails.Status = (int)typeof (BadHttpRequestException).GetProperty("StatusCode", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(badHttpRequestException);
problemDetails.Detail = badHttpRequestException.Message;
}
else
{
problemDetails.Title = "An unexpected error occurred!";
problemDetails.Status = 500;
problemDetails.Detail = "Testing"; //exception.Demystify().ToString();
}
// log the exception etc..
context.Response.StatusCode = problemDetails.Status.Value;
context.Response.WriteJson(problemDetails, "application/problem+json");
}
);
}
);
app.UseCommonResponser();
app.UseMvc();
}
}
// CommonResponseMiddleware
public class CommonResponseMiddleware
{
private readonly RequestDelegate _next;
private ICommonService _commonService;
public CommonResponseMiddleware(RequestDelegate next, ICommonService commonService)
{
_next = next;
_commonService = commonService;
}
public async Task Invoke(HttpContext context)
{
var currentBody = context.Response.Body;
using (var memoryStream = new MemoryStream())
{
context.Response.Body = memoryStream;
await _next.Invoke(context);
context.Response.Body = currentBody;
memoryStream.Seek(0, SeekOrigin.Begin);
var readToEnd = new StreamReader(memoryStream).ReadToEnd();
}
}
}
// GraphQLController
[Route("[controller]")]
[ApiController]
public class GraphQLController : Controller
{
private readonly IDocumentExecuter _documentExecuter;
private readonly ISchema _schema;
public GraphQLController(ISchema schema, IDocumentExecuter documentExecuter)
{
_schema = schema;
_documentExecuter = documentExecuter;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var inputs = query.Variables.ToInputs();
var executionOptions = new ExecutionOptions{Schema = _schema, Query = query.Query, Inputs = inputs, UserContext = Request.Headers, //UserContext = new GraphQLUserContext { Headers = Request.Headers }
};
throw new Exception("Exception while fetching all the students from the storage.");
var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
return BadRequest(result);
}
return Ok(result);
}
}
// HttpExtensions
public static class HttpExtensions
{
private static readonly JsonSerializer Serializer = new JsonSerializer{NullValueHandling = NullValueHandling.Ignore};
public static void WriteJson<T>(this HttpResponse response, T obj, string contentType = null)
{
response.ContentType = contentType ?? "application/json";
using (var writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8))
{
using (var jsonWriter = new JsonTextWriter(writer))
{
jsonWriter.CloseOutput = false;
jsonWriter.AutoCompleteOnClose = false;
Serializer.Serialize(jsonWriter, obj);
}
}
}
}
Now on invoking the graphqlcontroller endpoint, I am intentionally throwing an error which I am trying to catch in the global exception logic where I am trying to customize it before passing it to the customer.
I am getting an exception: Stream is now writable Parameter name: stream, while processing it in the HttpExtensions class at the below line:
using (var writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8))
Can anyone help me to fix this issue?

Related

how to retrieve actual error message in case of Bad Request using OwinContext middleware in .Net Web Api 2?

When .Net web API returns Badrequest("this is an example of Badrequest".), IOwinContext object in my middleware only contains context.Response.StatusCode 400 and context.Response.ReasonPhrase as "Bad Request". I want actual error message so that I can log it somewhere. Is it possible to get actual error message from IOwinContext without writing any custom class?
EDIT:
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
ICoreLogger dv_logger = new CoreLogger();
app.Use<InvalidAuthenticationMiddleware>(dv_logger);
ConfigureOAuth(app);
//register log4net
XmlConfigurator.Configure();
// configure log4net variables
GlobalContext.Properties["processId"] = "dv_logger";
System.Web.Http.GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
//entity framework
DbContext.Intialize();
AuthContext.Intialize();
.....
}
Middleware
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public override async Task Invoke(IOwinContext context)
{
var stream = context.Response.Body;
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
await Next.Invoke(context);
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responseBody = await reader.ReadToEndAsync();
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest)
{
var definition = new { Message = "" };
var error = JsonConvert.DeserializeAnonymousType(responseBody, definition);
Debug.WriteLine(error.Message);
}
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
}
}
Controller
public class LoginController : BaseController
{
[Route("")]
public IHttpActionResult Get(string email)
{
return BadRequest("this is an example of bad request!");
}
}
You will have to read the response's body. BadRequest("this is an example of BadRequest") sets an object in the body of the response with a property Message containing that message. I am assuming you are using JSON as a serialization format:
{
"Message": "this is an example of BadRequest"
}
Here is the middleware code to log the error message when the response is a BadRequest:
public override async Task Invoke(IOwinContext context)
{
var stream = context.Response.Body;
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
await next.Invoke();
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responseBody = await reader.ReadToEndAsync();
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest)
{
var definition = new { Message = "" };
var error = JsonConvert.DeserializeAnonymousType(responseBody, definition);
Console.WriteLine(error.Message);
}
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
}
Note that I used an anonymous type to avoid having to declare a class for the error message object.

Owin context.Response.Body always empty

have followed through some samples and creating a new buffer MemoryStream to replace the Response.Body before calling next().
Below is my middleware :
public class TrackingMiddleware
{
private readonly AppFunc next;
public TrackingMiddleware(AppFunc next)
{
this.next = next;
}
public async Task Invoke(IDictionary<string, object> env)
{
IOwinContext context = new OwinContext(env);
// Buffer the response
var stream = context.Response.Body;
var buffer = new MemoryStream();
context.Response.Body = buffer;
await this.next(env);
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
string responseBody = await reader.ReadToEndAsync();
buffer.Seek(0, SeekOrigin.Begin);
await buffer.CopyToAsync(stream);
}
}
the responsBody is always empty, event my ApiController's Action returned List of data.
Below is my Owin Startup class (Did i miss out anything ? )
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
static Startup()
{
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new OAuthAppProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(2),
AllowInsecureHttp = true
};
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseOAuthBearerTokens(OAuthOptions);
app.Use<TrackingMiddleware>();
}
}
Below is my ApiController
public class TestController : ApiController
{
public IHttpActionResult Get()
{
return Json(new string[] {"123", "asdfsdf"});
}
}
and My Web api configuration is registered through Global.asax.cs
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
Is this the reason that messed up the sequence of the middleware?
Alternatively, if i use MVC method and it works with the HttpContext.Current.Response by referencing following sample
You have to put the original stream back into the response
context.Response.Body = stream;
That way it can be read by the previous middleware in the pipeline
You would also want to dispose of any resources that are no longer needed
public async Task Invoke(IDictionary<string, object> env) {
IOwinContext context = new OwinContext(env);
// Buffer the response
var stream = context.Response.Body;
using (var buffer = new MemoryStream()) {
context.Response.Body = buffer;
//call next in pipeline
await this.next(env);
//reset the original response body
context.Response.Body = stream;
//get data from buffer
buffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(buffer);
string responseBody = await reader.ReadToEndAsync();
buffer.Seek(0, SeekOrigin.Begin);
//put data into original stream to continue the flow.
await buffer.CopyToAsync(stream);
}
}

C# DotNet Core Middleware Wrap Response

I have a simple controller action which looks like:
public Task<IEnumerable<Data>> GetData()
{
IEnumerable<Data> data = new List<Data>();
return data;
}
I want to be able to inspect the return value from within the middleware so the JSON would look something like
{
"data": [
],
"apiVersion": "1.2",
"otherInfoHere": "here"
}
So my payload always is within data. I know I can do this at a controller level but I don't wan to have to do it on every single action. I would rather do it in middleware once for all.
Here is an example of my middleware:
public class NormalResponseWrapper
{
private readonly RequestDelegate next;
public NormalResponseWrapper(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
var obj = context;
// DO something to get return value from obj
// Create payload and set data to return value
await context.Response.WriteAsync(/*RETURN NEW PAYLOAD HERE*/);
}
Any ideas?
Got the value now but it's to late to return it
try
{
using (var memStream = new MemoryStream())
{
context.Response.Body = memStream;
await next(context);
memStream.Position = 0;
object responseBody = new StreamReader(memStream).ReadToEnd();
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
// By now it is to late, above line sets the value that is going to be returned
await context.Response.WriteAsync(new BaseResponse() { data = responseBody }.toJson());
}
}
finally
{
context.Response.Body = originalBody;
}
Review the comments to get an understanding of what you can do to wrap the response.
public async Task Invoke(HttpContext context) {
//Hold on to original body for downstream calls
Stream originalBody = context.Response.Body;
try {
string responseBody = null;
using (var memStream = new MemoryStream()) {
//Replace stream for upstream calls.
context.Response.Body = memStream;
//continue up the pipeline
await next(context);
//back from upstream call.
//memory stream now hold the response data
//reset position to read data stored in response stream
memStream.Position = 0;
responseBody = new StreamReader(memStream).ReadToEnd();
}//dispose of previous memory stream.
//lets convert responseBody to something we can use
var data = JsonConvert.DeserializeObject(responseBody);
//create your wrapper response and convert to JSON
var json = new BaseResponse() {
data = data,
apiVersion = "1.2",
otherInfoHere = "here"
}.toJson();
//convert json to a stream
var buffer = Encoding.UTF8.GetBytes(json);
using(var output = new MemoryStream(buffer)) {
await output.CopyToAsync(originalBody);
}//dispose of output stream
} finally {
//and finally, reset the stream for downstream calls
context.Response.Body = originalBody;
}
}
In .NET Core 3.1 or .NET 5
Create your response envelope object. Example:
internal class ResponseEnvelope<T>
{
public T Data { set; get; }
public string ApiVersion { set; get; }
public string OtherInfoHere { set; get; }
}
Derive a class from ObjectResultExecutor
internal class ResponseEnvelopeResultExecutor : ObjectResultExecutor
{
public ResponseEnvelopeResultExecutor(OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory, ILoggerFactory loggerFactory, IOptions<MvcOptions> mvcOptions) : base(formatterSelector, writerFactory, loggerFactory, mvcOptions)
{
}
public override Task ExecuteAsync(ActionContext context, ObjectResult result)
{
var response = new ResponseEnvelope<object>();
response.Data = result.Value;
response.ApiVersion = "v1";
response.OtherInfoHere = "OtherInfo";
TypeCode typeCode = Type.GetTypeCode(result.Value.GetType());
if (typeCode == TypeCode.Object)
result.Value = response;
return base.ExecuteAsync(context, result);
}
}
Inject into the DI like
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IActionResultExecutor<ObjectResult>, ResponseEnvelopeResultExecutor>();
And the responses should have an envelope.
This does not work with primitive types.

OWIN OAuth JWT refresh token fails intermittently in load-balanced situation

I'm currently working on an OWIN OAuth implementation which uses JWT and supports token refreshing. I'm having intermittent problems with the token refresh process. The process works reliably on my development environment, but when published onto our Azure Service Fabric test environment, which is setup in a 3-node load-balanced configuration, the refresh token request often fails (not always!), and I get the infamous "invalid_grant" error.
I've found that the refresh token works successfully when being handled by the same service fabric node that issued it originally. However, it always fails when handled by a different node.
My understanding is that by using JWT, having a micro-service infrastructure deliver a load-balanced authentication server get's around the "machine-key" related issues that arise from using the OOTB access token format provided by OWIN.
Failed refresh tokens are making their way into the IAuthenticationTokenProvider.ReceiveAsync method, but the OAuthAuthorizationServerProvider.GrantRefreshToken method is never being hit, suggesting something in the OWIN middle-ware is not happy with the refresh token. Can anyone offer any insight into what the cause may be?
Now for the code, there's quite a bit - apologies for all the reading!
The authentication server is a service fabric stateless service, here's the ConfigureApp method:
protected override void ConfigureApp(IAppBuilder appBuilder)
{
appBuilder.UseCors(CorsOptions.AllowAll);
var oAuthAuthorizationServerOptions = InjectionContainer.GetInstance<OAuthAuthorizationServerOptions>();
appBuilder.UseOAuthAuthorizationServer(oAuthAuthorizationServerOptions);
appBuilder.UseJwtBearerAuthentication(InjectionContainer.GetInstance<JwtBearerAuthenticationOptions>());
appBuilder.UseWebApi(GetHttpConfiguration(InjectionContainer));
}
Here's the implementation of OAuthAuthorizationServerOptions:
public class AppOAuthOptions : OAuthAuthorizationServerOptions
{
public AppOAuthOptions(IAppJwtConfiguration configuration,
IAuthenticationTokenProvider authenticationTokenProvider,
IOAuthAuthorizationServerProvider authAuthorizationServerProvider)
{
AllowInsecureHttp = true;
TokenEndpointPath = "/token";
AccessTokenExpireTimeSpan = configuration.ExpirationMinutes;
AccessTokenFormat = new AppJwtWriterFormat(this, configuration);
Provider = authAuthorizationServerProvider;
RefreshTokenProvider = authenticationTokenProvider;
}
}
And here's the JwtBearerAuthenticationOptions implementation:
public class AppJwtOptions : JwtBearerAuthenticationOptions
{
public AppJwtOptions(IAppJwtConfiguration config)
{
AuthenticationMode = AuthenticationMode.Active;
AllowedAudiences = new[] {config.JwtAudience};
IssuerSecurityTokenProviders = new[]
{
new SymmetricKeyIssuerSecurityTokenProvider(
config.JwtIssuer,
Convert.ToBase64String(Encoding.UTF8.GetBytes(config.JwtKey)))
};
}
}
public class InMemoryJwtConfiguration : IAppJwtConfiguration
{
AppSettings _appSettings;
public InMemoryJwtConfiguration(AppSettings appSettings)
{
_appSettings = appSettings;
}
public int ExpirationMinutes
{
get { return 15; }
set { }
}
public string JwtAudience
{
get { return "CENSORED AUDIENCE"; }
set { }
}
public string JwtIssuer
{
get { return "CENSORED ISSUER"; }
set { }
}
public string JwtKey
{
get { return "CENSORED KEY :)"; }
set { }
}
public int RefreshTokenExpirationMinutes
{
get { return 60; }
set { }
}
public string TokenPath
{
get { return "/token"; }
set { }
}
}
And the ISecureData implementation:
public class AppJwtWriterFormat : ISecureDataFormat<AuthenticationTicket>
{
public AppJwtWriterFormat(
OAuthAuthorizationServerOptions options,
IAppJwtConfiguration configuration)
{
_options = options;
_configuration = configuration;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
var now = DateTime.UtcNow;
var expires = now.AddMinutes(_options.AccessTokenExpireTimeSpan.TotalMinutes);
var symmetricKey = Encoding.UTF8.GetBytes(_configuration.JwtKey);
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(symmetricKey),
SignatureAlgorithm, DigestAlgorithm);
var token = new JwtSecurityToken(
_configuration.JwtIssuer,
_configuration.JwtAudience,
data.Identity.Claims,
now,
expires,
signingCredentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
This is the IAuthenticationTokenProvider implementation:
public class RefreshTokenProvider : IAuthenticationTokenProvider
{
private readonly IAppJwtConfiguration _configuration;
private readonly IContainer _container;
public RefreshTokenProvider(IAppJwtConfiguration configuration, IContainer container)
{
_configuration = configuration;
_container = container;
_telemetry = telemetry;
}
public void Create(AuthenticationTokenCreateContext context)
{
CreateAsync(context).Wait();
}
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
try
{
var refreshTokenId = Guid.NewGuid().ToString("n");
var now = DateTime.UtcNow;
using (var container = _container.GetNestedContainer())
{
var hashLogic = container.GetInstance<IHashLogic>();
var tokenStoreLogic = container.GetInstance<ITokenStoreLogic>();
var userName = context.Ticket.Identity.FindFirst(ClaimTypes.UserData).Value;
var userToken = new UserToken
{
Email = userName,
RefreshTokenIdHash = hashLogic.HashInput(refreshTokenId),
Subject = context.Ticket.Identity.Name,
RefreshTokenExpiresUtc =
now.AddMinutes(Convert.ToDouble(_configuration.RefreshTokenExpirationMinutes)),
AccessTokenExpirationDateTime =
now.AddMinutes(Convert.ToDouble(_configuration.ExpirationMinutes))
};
context.Ticket.Properties.IssuedUtc = now;
context.Ticket.Properties.ExpiresUtc = userToken.RefreshTokenExpiresUtc;
context.Ticket.Properties.AllowRefresh = true;
userToken.RefreshToken = context.SerializeTicket();
await tokenStoreLogic.CreateUserTokenAsync(userToken);
context.SetToken(refreshTokenId);
}
}
catch (Exception ex)
{
// exception logging removed for brevity
throw;
}
}
public void Receive(AuthenticationTokenReceiveContext context)
{
ReceiveAsync(context).Wait();
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
try
{
using (var container = _container.GetNestedContainer())
{
var hashLogic = container.GetInstance<IHashLogic>();
var tokenStoreLogic = container.GetInstance<ITokenStoreLogic>();
var hashedTokenId = hashLogic.HashInput(context.Token);
var refreshToken = await tokenStoreLogic.FindRefreshTokenAsync(hashedTokenId);
if (refreshToken == null)
{
return;
}
context.DeserializeTicket(refreshToken.RefreshToken);
await tokenStoreLogic.DeleteRefreshTokenAsync(hashedTokenId);
}
}
catch (Exception ex)
{
// exception logging removed for brevity
throw;
}
}
}
And finally, this is the OAuthAuthorizationServerProvider implementation:
public class AppOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
if (context.ClientId != null)
{
context.Rejected();
return Task.FromResult(0);
}
// Change authentication ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
newIdentity.AddClaim(new Claim("newClaim", "refreshToken"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult(0);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
using (var container = _container.GetNestedContainer())
{
var requestedAuthenticationType = context.Request.Query["type"];
var requiredAuthenticationType = (int)AuthenticationType.None;
if (string.IsNullOrEmpty(requestedAuthenticationType) || !int.TryParse(requestedAuthenticationType, out requiredAuthenticationType))
{
context.SetError("Authentication Type Missing", "Type parameter is required to check which type of user you are trying to authenticate with.");
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
var authenticationWorker = GetInstance<IAuthenticationWorker>(container);
var result = await authenticationWorker.AuthenticateAsync(new AuthenticationRequestViewModel
{
UserName = context.UserName,
Password = context.Password,
IpAddress = context.Request.RemoteIpAddress ?? "",
UserAgent = context.Request.Headers.ContainsKey("User-Agent") ? context.Request.Headers["User-Agent"] : ""
});
if (result.SignInStatus != SignInStatus.Success)
{
context.SetError(result.SignInStatus.ToString(), result.Message);
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
// After we have successfully logged in. Check the authentication type for the just authenticated user
var userAuthenticationType = (int)result.AuthenticatedUserViewModel.Type;
// Check if the auth types match
if (userAuthenticationType != requiredAuthenticationType)
{
context.SetError("Invalid Account", "InvalidAccountForPortal");
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
var identity = SetClaimsIdentity(context, result.AuthenticatedUserViewModel);
context.Validated(identity);
}
}
public override async Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
using (var container = GetNestedContainer())
{
var email = context.Identity.FindFirst(ClaimTypes.UserData).Value;
var accessTokenHash = _hashLogic.HashInput(context.AccessToken);
var tokenStoreLogic = GetInstance<ITokenStoreLogic>(container);
await tokenStoreLogic.UpdateUserTokenAsync(email, accessTokenHash);
var authLogic = GetInstance<IAuthenticationLogic>(container);
var userDetail = await authLogic.GetDetailsAsync(email);
context.AdditionalResponseParameters.Add("user_id", email);
context.AdditionalResponseParameters.Add("user_name", userDetail.Name);
context.AdditionalResponseParameters.Add("user_known_as", userDetail.KnownAs);
context.AdditionalResponseParameters.Add("authentication_type", userDetail.Type);
}
await base.TokenEndpointResponse(context);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult(0);
}
private ClaimsIdentity SetClaimsIdentity(OAuthGrantResourceOwnerCredentialsContext context, AuthenticatedUserViewModel user)
{
var identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Name, context.UserName),
new Claim(ClaimTypes.SerialNumber, user.SerialNumber),
new Claim(ClaimTypes.UserData, user.Email.ToString(CultureInfo.InvariantCulture)),
new Claim(ClaimTypeUrls.AdminScope, user.Scope.ToString()),
new Claim(ClaimTypeUrls.DriverId, user.DriverId.ToString(CultureInfo.InvariantCulture)),
new Claim(ClaimTypeUrls.AdministratorId, user.AdministratorId.ToString(CultureInfo.InvariantCulture))
},
_authenticationType
);
//add roles
var roles = user.Roles;
foreach (var role in roles)
identity.AddClaim(new Claim(ClaimTypes.Role, role));
return identity;
}
}

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.

Categories