'Reading from request body in an authorization requirement handler breaks routing
I have a custom handler for security and an endpoint. It works as supposed to.
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AwoRequirement requirement)
{
HttpRequest request = Accessor.HttpContext.Request;
string awo = request.Headers
.SingleOrDefault(a => a.Key == "awo").Value.ToString();
...
bools authorized = ...;
if (authorized) context.Succeed(requirement);
else context.Fail();
return Task.CompletedTask;
}
[Authorize(Policy = "SomePolicy"), HttpPost("something")]
public async Task<IActionResult> Something([FromBody] Thing dto)
{ ... }
Now, I need to check the contents of the body, so I'm reading it in and analyze the contents. However, I noticed that with this addition, the endpoint isn't reached anymore. No exception or anything, just simply no hit, like if the route doesn't match. While debugging, I saw that the stream is used up so breakpointing the flow and reading again produces an empty string.
protected override Task HandleRequirementAsync( ... )
{
HttpRequest request = Accessor.HttpContext.Request;
...
using StreamReader stream = new StreamReader(Accessor.HttpContext.Request.Body);
string body = stream.ReadToEndAsync().Result;
Thing thing = JsonSerializer.Deserialize<Thing>(body);
if (thing.IsBad())
authorized &= fail;
...
return Task.CompletedTask;
}
According to this answer I should rewind seeking zero point of the stream, this one suggests enabling of buffering too. (There's also suggestion here but there's no await in the sample, which is required on my system, so I couldn't try it out properly.) Based on that, I landed in the following.
protected override Task HandleRequirementAsync( ... )
{
HttpRequest request = Accessor.HttpContext.Request;
...
Accessor.HttpContext.Request.EnableBuffering();
using StreamReader stream
= new StreamReader(Accessor.HttpContext.Request.Body);
string body = stream.ReadToEndAsync().Result;
Thing thing = JsonSerializer.Deserialize<Thing>(body);
if (thing.IsBad())
authorized &= fail;
...
return Task.CompletedTask;
}
Now, going back and rerunning the code does read in from the stream again. However, the endpoint isn't found anymore still, just like before adding the above. It is reached if I remove the reading from the stream, though, so I sense that I'm still affecting the body reading somehow.
Solution 1:[1]
I'm guessing you need to check if the user is allowed to perform an action on the submitted resource (Thing) according to a policy.
The way to go about this is to implement an IAuthorizationHandler, which lets you pass & inspect the resource in question.
Assume we have a Post class:
interface IAuthored
{
public string AuthorId { get; set; }
}
class Post : IAuthored
{
public string Title { get; set; }
public string AuthorId { get; set; }
}
We want to allow only the post author to make edits on it.
Here's the controller. I've added an [Authorize] to let only the authenticated users in.
public class PostController : ControllerBase
{
private AppDbContext _dbContext;
private IAuthorizationService _authorizationService;
public PostController(IAuthorizationService authorizationService, AppDbContext dbContext)
{
_authorizationService = authorizationService;
_dbContext = dbContext;
}
[Authorize] // this wouldn't work! [*]
[HttpPatch("{id}")]
public async Task<ActionResult> EditPost(string id)
{
var post = await _dbContext.Set<Post>().FindAsync(id);
// oops! any authenticated user can edit this post.
post.Title = "asd";
await _dbContext.SaveChangesAsync();
return Ok();
}
}
Normally, with a simple policy, we'd annotate the action with [Authorize("my_policy")]. But it wouldn't work here, because [Authorize] attributes are evaluated (in authorization middleware) before the execution reaches the controller. ASP.NET Core (or you) cannot know which resource is being handled [*].
So we need to imperatively authorize the action. We define a requirement, and a policy that enforces it.
// just a marker class
class AuthorRequirement : IAuthorizationRequirement
{
}
services.AddAuthorization(
options => {
options.AddPolicy("editor", builder => builder.Requirements.Add(new AuthorRequirement()));
}
);
Then implement a handler for this requirement. We can subclass AuthorizationHandler<TRequirement, TResource> or AuthorizationHandler<TRequirement>. I'm opting to authorize all classes that implement IAuthored interface.
class AuthorRequirementHandler : AuthorizationHandler<AuthorRequirement, IAuthored> // for a specific
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorRequirement requirement, IAuthored resource)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (resource.AuthorId == userId)
{
context.Succeed(requirement);
}
// ... let other handlers take a stab at this
return Task.CompletedTask;
}
}
This works, but we're forced to handle authorization imperatively inside the action.
[*]: If we can infer the resource before it hits the endpoint, we can short-circuit the whole operation.
Let's create an extension method that lets us read the request body multiple times, and enable request buffering.
internal static class HttpRequestExtensions {
public static async Task<T> ReadAsJsonAsync<T>(this HttpRequest request, JsonSerializerOptions options = null)
{
request.Body.Position = 0;
var result = await request.ReadFromJsonAsync<T>(options);
// reset the position again to let endpoint middleware read it
request.Body.Position = 0;
return result;
}
}
app.Use((context, next) => {
context.Request.EnableBuffering(1_000_000);
return next();
});
app.UseAuthorization();
Now we can rewrite the handler to read the body and perform authorization "declaratively" [^1].
class AuthorRequirementHandler : AuthorizationHandler<AuthorRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthorRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorRequirement requirement)
{
var endpoint = _httpContextAccessor.HttpContext.GetEndpoint();
var action = endpoint.Metadata.OfType<ControllerActionDescriptor>().FirstOrDefault();
// is the action is expecting a post DTO
if (!action.Parameters.Any(p => p.ParameterType == typeof(Post)))
{
return;
}
var post = await _httpContextAccessor!.HttpContext!.Request.ReadAsJsonAsync<Post>();
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (post.AuthorId == userId)
{
context.Succeed(requirement);
}
}
}
Once the user is authorized, the request follows the middleware chain and reaches the EndpointMiddleware, which reads & parses the request again, and delegates it to the controller action.
[^1]: I'd actually advise against this approach as it mixes authorization, which is a business requirement, with implementation details (HTTP) that it shouldn't know anything about. It's also not testable, unlike the previous approach.
Solution 2:[2]
You can check the code here. Either use the nuget package or just copy the method you need and if you wish to follow latest trends, rewrite it using Memory<byte>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | yanislavgalyov |
