I have a middleware to log web api requests. Below is the Configuration method inside Startup.cs. If app.UseMiddleware comes before app.UseMvc none of the web api calls get invoked However, if app.UseMiddlewarecomes after app.UseMvc, the middleware does not do anything (i.e., recording requests).
I provided the code below. Any ideas why app.UseMiddleware interfers with asp.UseMvc?
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider services)
{
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseMiddleware<ApiLoggingMiddleware>();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
Below is the middleware:
public async Task Invoke(HttpContext httpContext, IApiLogService apiLogService)
{
try
{
_apiLogService = apiLogService;
var request = httpContext.Request;
if (request.Path.StartsWithSegments(new PathString("/api")))
{
var stopWatch = Stopwatch.StartNew();
var requestTime = DateTime.UtcNow;
var requestBodyContent = await ReadRequestBody(request);
var originalBodyStream = httpContext.Response.Body;
await SafeLog(requestTime,
stopWatch.ElapsedMilliseconds,
200,//response.StatusCode,
request.Method,
request.Path,
request.QueryString.ToString(),
requestBodyContent
);
}
else
{
await _next(httpContext);
}
}
catch (Exception ex)
{
await _next(httpContext);
}
}
You have to always call await _next(httpContext); in middleware otherwise request does not go down the pipeline:
public async Task Invoke(HttpContext httpContext, IApiLogService apiLogService)
{
try
{
_apiLogService = apiLogService;
var request = httpContext.Request;
if (request.Path.StartsWithSegments(new PathString("/api")))
{
var stopWatch = Stopwatch.StartNew();
var requestTime = DateTime.UtcNow;
var requestBodyContent = await ReadRequestBody(request);
var originalBodyStream = httpContext.Response.Body;
await SafeLog(requestTime,
stopWatch.ElapsedMilliseconds,
200,//response.StatusCode,
request.Method,
request.Path,
request.QueryString.ToString(),
requestBodyContent
);
};
}
catch (Exception ex)
{
}
await _next(httpContext);
}
Edit (simple explanation of middleware):
The whole middleware thing works in the following way - When request comes to your application, it goes through middleware pipeline, where each middleware has to invoke next middleware in order to finally get the request to your controller. When you invoke await _next(httpContext); you are basically calling Invoke method of the next middleware in the pipeline. If you do not call await _next(httpContext); you are stopping the request and it wont come to your controller. One thing to notice is that when await _next(httpContext); returns, the request has already been served by your controller.
Related
In versions before .net6, I used to add an ErrorHandlerMiddleware to the pipeline, so I can centralize the need of returning error types in the application with different status codes.
an example is something exactly like this:
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception error)
{
string responseMessage = error.Message;
string exceptionMessage;
object ResponseObject;
var response = context.Response;
response.ContentType = "application/json";
switch (error)
{
case ApplicationBadRequestException e:
if (e.metaData is { })
ResponseObject = new { responseMessage, e.metaData };
else ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status400BadRequest;
break;
case ApplicationNotFoundException e:
if (e.metaData is { })
ResponseObject = new { responseMessage, e.metaData };
else ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status404NotFound;
break;
case ApplicationUnAuthorizedException e:
responseMessage = "You are not authorized to perform this operation.";
ResponseObject = new { responseMessage };
response.StatusCode = StatusCodes.Status401Unauthorized;
break;
default:
responseMessage = "An error has occurred...Please try again later.";
exceptionMessage = getExMessage(error);
ResponseObject = new { responseMessage, exceptionMessage };
response.StatusCode = (int)HttpStatusCode.InternalServerError;
break;
}
var result = JsonSerializer.Serialize(ResponseObject);
await response.WriteAsync(result);
}
}
}
Then at any controller in my application, when I need to return a certain error code in the response - for example 400 BadRequest - I do it like so
throw new ApplicationBadRequestException("this is the error message");
This causes the middleware pipeline to be reversed and this exception is fetched inside the ErrorhandlerMiddleware.
Now I am trying to use GraphQL in.net6, when I make this I can see that the Middleware pipeline is NOT reversed and the ErrorHandlerMiddleware class is not invoked. why does this happen?
EDIT: I have tried the same scenario with rest apis and the code works perfectly, seems that the problem happens due to the existence of GraphQL configuration.
this is my Program.cs file
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var connectionString = builder.Configuration["ConnectionStrings:DBConnectionString"];
builder.Services.AddDbContextFactory<DBEntities>(options => options.UseSqlServer(connectionString, x => x.UseNetTopologySuite()));
builder.Services.AddScoped<DBEntities>(options => options.GetRequiredService<IDbContextFactory<DBEntities>>().CreateDbContext());
builder.Services.AddGraphQLServer().AddQueryType<Query>().AddMutationType<Mutation>()
.AddProjections().AddFiltering().AddSorting();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseMiddleware<ErrorHandlerMiddleware>();
app.MapGraphQL("/graphql");
app.Run();
Im trying to set up IAsyncActionFilter to log request body for some API requests.
But when I try to read body stream I get empty string every time.
Here is my code:
StringContent is always an empty string, even tho there is an json body on post requests.
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ActionExecutedContext rContext = null;
string stringContent = string.Empty;
try
{
context.HttpContext.Request.EnableBuffering();
context.HttpContext.Request.Body.Position = 0;
using (var reader = new StreamReader(context.HttpContext.Request.Body))
{
stringContent = await reader.ReadToEndAsync();
context.HttpContext.Request.Body.Position = 0;
}
rContext = await next();
}
catch (Exception)
{
throw;
}
}
I dont want to use middleware, becouse I need to log only some of the controllers.
The request body has already been read by the MVC model binder by the time your IAsyncActionFilter executes https://github.com/aspnet/Mvc/issues/5260.
As a quick workaround, you could add
app.Use(next => context =>
{
context.Request.EnableBuffering();
return next(context);
});
in your startup.cs Configure() BEFORE your UseEndpoints call
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.Use(next => context =>
{
context.Request.EnableBuffering();
return next(context);
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Of course, this would enable request body buffering for all of your requests, which may not be desirable. If that's the case, you would have to add some conditional logic to the EnableBuffering delegate.
I apologize in advance for my English.
I have to develop a web api that uses Oauth 2.0 to authenticate itself on an external site. Next I have to use the access token that is returned to me to send requests to the same site. I'm doing a test using the github API.
This is the Startup class:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "GitHub";
})
.AddCookie()
.AddOAuth("GitHub", options =>
{
options.ClientId = Configuration["GitHub:ClientId"];
options.ClientSecret = Configuration["GitHub:ClientSecret"];
options.CallbackPath = new PathString("/signin-github");
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.SaveTokens = true;
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("urn:github:login", "login");
options.ClaimActions.MapJsonKey("urn:github:url", "html_url");
options.ClaimActions.MapJsonKey("urn:github:avatar", "avatar_url");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
Console.WriteLine("This is the access Token: " + context.AccessToken);
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
}
with this:
options.SaveTokens = true;
the token should be saved in AuthenticationProperties, but in my Controller I don't know how to access the token in order to pass it in the headers for requests.
The only thing I have found is an obsolete method, that is this:
var authenticateInfo = await HttpContext.Authentication.GetAuthenticateInfoAsync("Bearer");
string accessToken = authenticateInfo.Properties.Items[".Token.access_token"];
but i have this error:
No authentication handler is configured to authenticate for the scheme: Bearer
This is my Typed Client
public class Service
{
public HttpClient Client { get; }
public Service(HttpClient client)
{
var token = GetTokenAsync();
client.DefaultRequestHeaders.Add("Authorization", ""); //here I have to pass the access token
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "{subscription key}");
Client = client;
}
public async Task<string> GetTokenAsync()
{
//I want the access token returned to me
return token;
}
}
You can get the access token via :
var token = await HttpContext.GetTokenAsync("access_token");
I wanted to add a principle onto the thread by myself , without using the Identity mechanism which really reminds me the old membership/forms authentication mechanics.
So I've managed (successfully) to create a request with principle :
MyAuthMiddleware.cs
public class MyAuthMiddleware
{
private readonly RequestDelegate _next;
public MyAuthMiddleware(RequestDelegate next )
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var claims = new List<Claim>
{
new Claim("userId", "22222222")
};
ClaimsIdentity userIdentity = new ClaimsIdentity(claims ,"MyAuthenticationType");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
httpContext.User = principal;
await _next(httpContext);
}
}
The Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//app.UseAuthentication(); //removed it. I will set the thread manually
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseMyAuthMiddleware(); // <---------- Myne
app.UseMvc();
app.Run(async context => { await context.Response.WriteAsync("Hello World!"); });
}
And here is the Action in the Controller: (Notice Authorize attribute)
[HttpGet]
[Route("data")]
[Authorize]
public IActionResult GetData()
{
var a=User.Claims.First(f => f.Type == "userId");
return new JsonResult(new List<string> {"a", "b",a.ToString() , User.Identity.AuthenticationType});
}
Ok Let's try calling this method, Please notice that this does work :
So where is the problem 😊?
Please notice that there is an [Authorize] attribute. Now let's remove
setting principle on the thread ( by removing this line ) :
//httpContext.User = principal; // line is remarked
But now when I navigate to :
http://localhost:5330/api/cities/data
I'm being redirected to :
http://localhost:5330/Account/Login?ReturnUrl=%2Fapi%2Fcities%2Fdata
But I'm expecting to see Unauthorized error.
I'm after WebApi alike responses. This is not a website but an API.
Question:
Why don't I see the Unauthorized error ? And how can I make it appear?
Nb here is my ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication( CookieAuthenticationDefaults.AuthenticationScheme )
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, a =>
{
a.LoginPath = "";
a.Cookie.Name = "myCookie";
});
services.AddMvc();
}
EDIT
Currently What I've managed to do is to use OnRedirectionToLogin :
But it will be really disappointing if that's the way to go. I'm expecting it to be like webapi.
The default implementation of the OnRedirectToLogin delegate looks like this:
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
As is clear from the code above, the response that gets sent to the client is dependent upon the result of IsAjaxRequest(...), which itself looks like this:
private static bool IsAjaxRequest(HttpRequest request)
{
return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
}
This means that the response will be a 401 redirect if either a X-Requested-With request header or query-string value is set to XMLHttpRequest. When you hit your endpoint directly from the browser or from within e.g. Fiddler, this value is not set and so the response is a 302, as observed. Otherwise, when using XHR or Fetch in the browser, this value gets set for you as a header and so a 401 is returned.
The following program creates a simple ASP.NET MVC Core 2.2.0 site with two routes:
/success - returns the string "Success Response".
/failure - throws an exception, causing a 500 to be returned
There is additionally middleware which sets a HTTP response header of X-Added-Key/X-Added-Value. It does this using the OnStarting event, which, according to the documentation "Adds a delegate to be invoked just before response headers will be sent to the client.".
However, what I'm seeing is that my HTTP response header is erased when MVC handles an exception for the /failure route; the middleware is definitely hit, but the response is missing the X-Added-Key HTTP response header. I suspect this is being done here.
How should I set a HTTP response header so that MVC does not discard it in the event of an exception? My use case here is that we're looking to return a request ID so that API consumers can give us a reference if they would like to report a bug, so it should be returned regardless of whether the controller encountered an exception.
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
namespace ResponseHeadersSanityCheck
{
internal static class Program
{
static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.Run();
}
}
public sealed class ExampleController : Controller
{
[HttpGet, Route("success")]
public string Success() => "Success Response";
[HttpGet, Route("failure")]
public string Failure() => throw new Exception("Failure Response");
}
public sealed class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app
.Use(async (HttpContext context, Func<Task> next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Added-Key", "X-Added-Value");
return Task.CompletedTask;
});
await next();
})
.UseMvc();
}
}
}
You could capture the exception between the middleware handling.
Try
app.Use(async (HttpContext context, Func<Task> next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Added-Key", "X-Added-Value");
return Task.CompletedTask;
});
try
{
await next();
}
catch (Exception ex)
{
}
}).UseMvc();
Update:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.Use(async (HttpContext context, Func<Task> next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Added-Key", "X-Added-Value");
return Task.CompletedTask;
});
await next();
}).UseMvc();
}
Update:
app.Use(async (HttpContext context, Func<Task> next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Added-Key", "X-Added-Value");
return Task.CompletedTask;
});
//await next();
try
{
await next();
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
}
}).UseMvc();