24

I am using Openidict.
I am trying to return custom message with custom status code, but I am unable to do it. My configuration in startup.cs:

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(o =>
            {
                o.Authority = this.Configuration["Authentication:OpenIddict:Authority"];
                o.Audience = "MyApp";           //Also in Auhorization.cs controller.
                o.RequireHttpsMetadata = !this.Environment.IsDevelopment();
                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = context =>
                    {
                        context.Response.StatusCode = HttpStatusCodes.AuthenticationFailed;
                        context.Response.ContentType = "application/json";
                        var err = this.Environment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
                        var result = JsonConvert.SerializeObject(new {err});
                        return context.Response.WriteAsync(result);
                    }
                };
            });

But the problem is no content is returned. Chrome developer tools report

(failed)

error

for Status and

Failed to load response data

error

for response.

I also tried:

context.Response.WriteAsync(result).Wait();
return Task.CompletedTask;

but the result is the same.

Desired behaviour:
I would like to return custom status code with message what went wrong.

Rob
  • 14,746
  • 28
  • 47
  • 65
Makla
  • 9,899
  • 16
  • 72
  • 142
  • 1
    [**Do not post images of code or errors!**](https://meta.stackoverflow.com/q/303812/995714) Images and screenshots can be a nice addition to a post, but please make sure the post is still clear and useful without them. If you post images of code or error messages make sure you also copy and paste or type the actual code/message into the post directly. – Rob Feb 06 '18 at 18:43
  • It's failing to load `vendor.js`. Are you using `app.UseStaticFiles()` in your `Startup.cs` file? If so, it must be registered before `app.UseAuthentication()`. – Brad Feb 07 '18 at 01:55
  • 1
    OnAuthenticationFailed for JwtBearer can't return content anymore in 2.0. it doesn't have the necessity control over the request flow. – Tratcher Feb 07 '18 at 02:37
  • @Rob Understand, I wasn't sure if text will be enough. It is really strange for me, to get failed for status code, although I discovered that actual code is returned, but after Angular process request that code is changed to failed. – Makla Feb 07 '18 at 05:50
  • @Brad I am using `UseStaticFiles` and it is registered before `UseAuthentication`. It is not failing to load `vendor` but error is raised from `vendor`. – Makla Feb 07 '18 at 05:54
  • I am not sure why but the token expiration validation failed and yet I am able to consume api – Silly Volley Mar 20 '20 at 09:28
  • Thanks for this question. The answers helped me with the solution. – Jeremy Ray Brown Jan 12 '21 at 01:26

6 Answers6

27

It's important to note that both the aspnet-contrib OAuth2 validation and the MSFT JWT handler automatically return a WWW-Authenticate response header containing an error code/description when a 401 response is returned:

enter image description here

If you think the standard behavior is not convenient enough, you can use the events model to manually handle the challenge. E.g:

services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "http://localhost:54540/";
        options.Audience = "resource_server";
        options.RequireHttpsMetadata = false;
        options.Events = new JwtBearerEvents();
        options.Events.OnChallenge = context =>
        {
            // Skip the default logic.
            context.HandleResponse();

            var payload = new JObject
            {
                ["error"] = context.Error,
                ["error_description"] = context.ErrorDescription,
                ["error_uri"] = context.ErrorUri
            };

            context.Response.ContentType = "application/json";
            context.Response.StatusCode = 401;

            return context.Response.WriteAsync(payload.ToString());
        };
    });
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • How do I know if there was an error? Does `HandleResponse` return anything useful? – Makla Feb 12 '18 at 13:34
  • 2
    `HandleResponse` doesn't return anything (I agree it's a poor name). It just tells the JWT handler you want to take care of the response and don't want the default logic (returning a `WWW-Authenticate` header) to be applied. – Kévin Chalet Feb 12 '18 at 17:43
  • 2
    Without the HandleResponse(), I'm getting error logs "System.InvalidOperationException: StatusCode cannot be set because the response has already started.". By adding it, I'm no longer seeing that error. That line was really helpful to me. Thanks. – Kes Feb 19 '19 at 05:05
  • @KévinChalet thanks for your answer, is there a way to return a json and not a text? – yavg Aug 24 '20 at 06:06
  • @yavg just make sure you return a `Content-Type` header with `application/json`. I updated my answer to reflect that. – Kévin Chalet Aug 24 '20 at 13:50
  • Sorry to bother you again, do you know how I can modify the code? doing this returns 200 – yavg Aug 24 '20 at 15:54
26

Was facing same issue, tried the solution provided by Pinpoint but it didnt work for me on ASP.NET core 2.0. But based on Pinpoint's solution and some trial and error, the following code works for me.

var builder = services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(o =>
        {
            o.Authority = "http://192.168.0.110/auth/realms/demo";
            o.Audience = "demo-app";
            o.RequireHttpsMetadata = false;

            o.Events = new JwtBearerEvents()
            {
                OnAuthenticationFailed = c =>
                {
                    c.NoResult();
                    c.Response.StatusCode = 500;
                    c.Response.ContentType = "text/plain";
                    c.Response.WriteAsync(c.Exception.ToString()).Wait();
                    return Task.CompletedTask;
                },
                OnChallenge = c =>
                {
                    c.HandleResponse();
                    return Task.CompletedTask;
                }
            };
        });
Ryan Teh
  • 519
  • 5
  • 19
7

This is what worked for me after finding issues related to this exception that seemed to appear after updating packages.

System.InvalidOperationException: StatusCode cannot be set because the response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
   at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)

The implementation is below,

                OnAuthenticationFailed = context =>
                {
                    context.NoResult();
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    context.Response.ContentType = "application/json";

                    string response =
                        JsonConvert.SerializeObject("The access token provided is not valid.");
                    if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                    {
                        context.Response.Headers.Add("Token-Expired", "true");
                        response =
                            JsonConvert.SerializeObject("The access token provided has expired.");
                    }

                    context.Response.WriteAsync(response);
                    return Task.CompletedTask;
                },
                OnChallenge = context =>
                {
                    context.HandleResponse();
                    return Task.CompletedTask;
                }
Tyler V
  • 272
  • 3
  • 11
2

please check with the bellow code for .net core 2.1

OnAuthenticationFailed =context =>
                {
                    context.Response.OnStarting(async () =>
                    {
                        context.NoResult();
                        context.Response.Headers.Add("Token-Expired", "true");
                        context.Response.ContentType = "text/plain";
                        context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                        await context.Response.WriteAsync("Un-Authorized");
                    });

                    return Task.CompletedTask;                        
                },
2

In my opinion this is the correct solution. The important part is c.Response.CompleteAsync().Wait();

Tested with .NET7

Be careful when using OnChallenge as with the other solutions. It overwrites the missing access token response 401.

In dev mode, the exception is even returned with the following snippet.

o.Events = new JwtBearerEvents()
        {
            OnAuthenticationFailed = c =>
            {
                if (c.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    c.NoResult();
                    c.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    c.Response.ContentType = "text/plain";
                    c.Response.WriteAsync("The token is expired").Wait();
                    c.Response.CompleteAsync().Wait();

                    return Task.CompletedTask;
                }
                else if(app.Environment.IsDevelopment() == false)
                {
                    c.NoResult();
                    c.Response.StatusCode = StatusCodes.Status500InternalServerError;
                    c.Response.ContentType = "text/plain";
                    c.Response.WriteAsync("An error occurred processing your authentication.").Wait();
                    c.Response.CompleteAsync().Wait();

                    return Task.CompletedTask;
                }
                else
                {
                    c.NoResult();
                    c.Response.StatusCode = StatusCodes.Status500InternalServerError;
                    c.Response.ContentType = "text/plain";
                    c.Response.WriteAsync(c.Exception.ToString()).Wait();
                    c.Response.CompleteAsync().Wait();

                    return Task.CompletedTask;

                }
            }
TheBestGin
  • 21
  • 2
0

Below code work with .Net 6(minimal API

var app = builder.Build();
app.Use(async (context, next) =>
{
    await next();

    if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) // 401
    {
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonConvert.SerializeObject(new Error()
        {
            Message = "Token is not valid"
        }));
    }
});
  • 1
    JSON is built in: ```await context.Response.WriteAsJsonAsync(new Error() { Message = "Token is not valid"});``` – davidfowl Oct 01 '22 at 03:36
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 05 '22 at 01:06