question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

InvalidOperationException when using JwtBearerEvents.OnForbidden to customize response and multiple AuthenticationSchemes

See original GitHub issue

Describe the bug

Our project has a requirement that all error responses, including those related to authentication, include a standard error object in the response body. In order to satisfy this requirement, we implement a couple of JwtBearerEvents events as follows (tokenType is defined somewhere else):

new JwtBearerEvents {
  OnChallenge = async context => {
    if (!context.Response.HasStarted) {
      // This can fire if the Authorization header is missing or malformed, handle those differently.
      var err = !string.IsNullOrEmpty(context.Request.Headers["Authorization"])
        ? new ServiceError(ErrorCode.INVALID_AUTHORIZATION, $"An invalid {tokenType} was included in the request.")
        : new ServiceError(ErrorCode.MISSING_AUTHORIZATION, $"An {tokenType} must be included in the request.");
      context.Response.StatusCode = StatusCodes.Status401Unauthorized;
      context.Response.ContentType = MediaTypeNames.Application.Json;
      await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(err)));
    }
    context.HandleResponse();
  },
  OnForbidden = async context => {
    var err = new ServiceError(ErrorCode.INVALID_AUTHORIZATION, $"An invalid {tokenType} was included in the request.");
    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
    context.Response.ContentType = MediaTypeNames.Application.Json;
    await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(err)));
  }
}

This generally works great, and does exactly what we need. However, we’ve discovered that this causes issues for one very specific case: when an action’s AuthorizeAttribute has multiple schemes, for example [Authorize(AuthenticationSchemes = "FooScheme,BarScheme", Policy = "SomePolicy")], and an authentication attempt is denied by the specified Policy.

When this happens, the OnForbidden event is invoked once, for the first scheme (“FooScheme” here), and then the entire request fails with the following (somewhat trimmed):

  Message: 
    System.InvalidOperationException : The status code cannot be set, the response has already started.

  Stack Trace:
    ResponseFeature.set_StatusCode(Int32 value)
    DefaultHttpResponse.set_StatusCode(Int32 value)
    JwtBearerHandler.HandleForbiddenAsync(AuthenticationProperties properties)
    AuthenticationHandler`1.ForbidAsync(AuthenticationProperties properties)
    AuthenticationService.ForbidAsync(HttpContext context, String scheme, AuthenticationProperties properties)
    AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    AuthorizationMiddleware.Invoke(HttpContext context)
    AuthenticationMiddleware.Invoke(HttpContext context)
    DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
    DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
    <<SendAsync>g__RunRequestAsync|0>d.MoveNext()

After digging into the code mentioned in the stack trace using the debugger, I’ve determined that the issue seems to be a combination of two things:

  1. First, in AuthorizationMiddlewareResultHandler.HandleAsync(), the “else if” branch has logic that will invoke context.ForbidAsync() for all schemes, if there are multiple.
  2. Second, and more importantly, is what JwtBearerHandler is doing here:
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
    var forbiddenContext = new ForbiddenContext(Context, Scheme, Options);
    Response.StatusCode = 403;
    return Events.Forbidden(forbiddenContext);
}

It’s clear to me that the reason I’m getting the exception described above is because, when AuthorizationMiddlewareResultHandler.HandleAsync() calls context.ForbidAsync() for the second scheme, the response body has already been written by the first invocation of our OnForbidden event implementation…which means that JwtBearerHandler.HandleForbiddenAsync() is going to fail since it’s trying to set StatusCode again.

Compared to the logic used by JwtBearerHandler.HandleChallengeAsync(), the implementation of HandleForbiddenAsync() seems rather…lackluster. It seems to me that it would be helpful if HandleForbiddenAsync() and the ForbiddenContext object would expose a mechanism similar to that used by HandleChallengeAsync()/JwtBearerChallengeContext, whereby the OnForbidden event implementation could have a chance to call context.ResponseHandled(), which in turn would signal to the HandleForbiddenAsync() method that it should not attempt to do anything to the response. This would allow us to check context.Response.HasStarted in our event implementation and do nothing (both in the event, and in HandleForbiddenAsync()) if it returns true.

Could such a mechanism be added? The only alternative I’ve been able to find for our project is to not use multiple schemes in a single AuthorizeAttribute, but since going that route would require us to create a separate action (and thus request path) for every affected action…it’s definitely not desirable.

PS: Apologies if this doesn’t quite fit the usual “Bug Report” bucket; I know it’s somewhere between that and a feature request, but felt it leaned more towards the bug side of the spectrum.

Further technical details

  • ASP.NET Core 5.0.300
  • Microsoft.AspNetCore.Authentication.JwtBearer 5.0.6
  • Visual Studio 2019 (16.10.0)

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:11 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
adityamandaleekacommented, Sep 1, 2021

@wcontayon Just checking, are you still interested in working on this?

0reactions
lukefullitoncommented, Mar 8, 2023

Hello @Tratcher , I submitted a PR with the requested change since it looks like this issue is still open and it has been a couple years since the last community comments. I hope this is okay. Please let me know if you would like me to make any changes to my PR here https://github.com/dotnet/aspnetcore/pull/47089. Thanks

Here was the requested change for reference, based on the comment on 6/17/21. HandleForbiddenAsync could check Response.HasStarted before setting the status code. If the response had started it would log instead. The event logic would then run as normal.

Read more comments on GitHub >

github_iconTop Results From Across the Web

AddJwtBearer OnAuthenticationFailed return custom error
Without the HandleResponse(), I'm getting error logs "System.InvalidOperationException: StatusCode cannot be set because the response has ...
Read more >
Authorize with a specific scheme in ASP.NET Core
This article explains how to limit identity to a specific scheme when working with multiple authentication methods.
Read more >
JWT Bearer Authentication and Authorization for ASP.NET ...
An introduction on how to configure JWT Bearer authentication and authorization (based on scopes) for your ASP.NET Core 5 APIs.
Read more >
How to Use Multiple Authentication Schemes in .NET
To demonstrate how multiple schemes can work together, we are going to implement an API that uses cookie-based authentication with the default ...
Read more >
Working with custom authentication schemes in ASP.NET ...
This article is an introduction on how to use custom authentication schemes to build a simple web application with authentication.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found