Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom 401 and 403 response model with UseJwtBearerAuthentication middleware

I want to respond with a JSON response model when a 401 and 403 occur. For example:

HTTP 401
{
  "message": "Authentication failed. The request must include a valid and non-expired bearer token in the Authorization header."
}

I am using middleware (as suggested in this answer) to intercept 404s and it works great, but it is not the case with 401 or 403s. Here is the middleware:

app.Use(async (context, next) =>
{
    await next();
    if (context.Response.StatusCode == 401)
    {
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonConvert.SerializeObject(UnauthorizedModel.Create(), SerializerSettings), Encoding.UTF8);
    }
});

When placed BELOW app.UseJwtBearerAuthentication(..) in Startup.Configure(..), it seems to be completely ignored and a normal 401 is returned.

When placed ABOVE app.UseJwtBearerAuthentication(..) in Startup.Configure(..), then the following exception is thrown:

Connection id "0HKT7SUBPLHEM": An unhandled exception was thrown by the application. System.InvalidOperationException: Headers are read-only, response has already started. at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.FrameHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value) at Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_ContentType(String value) at MyProject.Api.Startup.<b__12_0>d.MoveNext() in Startup.cs

like image 466
Dave New Avatar asked Jul 09 '16 11:07

Dave New


2 Answers

Set was on the right track, but there's actually no need to create your own middleware, as you can leverage the events model to override the default challenge logic.

Here's an example that will return a 401 response containing the OAuth2 error code/description as plain text (you can of course return JSON or whatever you want):

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    Authority = "http://localhost:54540/",
    Audience = "http://localhost:54540/",
    RequireHttpsMetadata = false,
    Events = new JwtBearerEvents
    {
        OnChallenge = async context =>
        {
            // Override the response status code.
            context.Response.StatusCode = 401;

            // Emit the WWW-Authenticate header.
            context.Response.Headers.Append(
                HeaderNames.WWWAuthenticate,
                context.Options.Challenge);

            if (!string.IsNullOrEmpty(context.Error))
            {
                await context.Response.WriteAsync(context.Error);
            }

            if (!string.IsNullOrEmpty(context.ErrorDescription))
            {
                await context.Response.WriteAsync(context.ErrorDescription);
            }

            context.HandleResponse();
        }
    }
});

Alternatively, you can also use the status code pages middleware, but for 403 responses, you won't have any hint about the authorization policy that caused it:

app.UseStatusCodePages(async context =>
{
    if (context.HttpContext.Request.Path.StartsWithSegments("/api") &&
       (context.HttpContext.Response.StatusCode == 401 ||
        context.HttpContext.Response.StatusCode == 403))
    {
        await context.HttpContext.Response.WriteAsync("Unauthorized request");
    }
});
like image 66
Kévin Chalet Avatar answered Nov 17 '22 23:11

Kévin Chalet


First of all, order of middlewares is important.

Each middleware chooses whether to pass the request on to the next component in the pipeline, and can perform certain actions before and after the next component is invoked in the pipeline

UseJwtBearerAuthentication stops further pipeline execution if error occurred.

But your approach does not work with JwtBearerAuthentication middleware, as when you have unauthorized error, middleware sends WWWAuthenticate header, that why you get "response has already started" exception - look into HandleUnauthorizedAsync method. You can override this method and implement your own custom logic.

Another possible solution (not sure that works) is to use HttpContext.Response.OnStarting callback in your middleware, as it is called before header send. You cal look on this SO answer

like image 1
Set Avatar answered Nov 17 '22 22:11

Set