AddJwtBearer OnAuthenticationFailed return custom error
Asked Answered
M

6

26

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.

Mop answered 6/2, 2018 at 18:35 Comment(7)
Do not post images of code or errors! 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.Phylactery
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().Cordeelia
OnAuthenticationFailed for JwtBearer can't return content anymore in 2.0. it doesn't have the necessity control over the request flow.Interferometer
@Phylactery 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.Mop
@Cordeelia I am using UseStaticFiles and it is registered before UseAuthentication. It is not failing to load vendor but error is raised from vendor.Mop
I am not sure why but the token expiration validation failed and yet I am able to consume apiTaxaceous
Thanks for this question. The answers helped me with the solution.Biogeochemistry
M
32

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());
        };
    });
Militant answered 11/2, 2018 at 20:25 Comment(6)
How do I know if there was an error? Does HandleResponse return anything useful?Mop
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.Stressful
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.Larose
@KévinChalet thanks for your answer, is there a way to return a json and not a text?Outpatient
@Outpatient just make sure you return a Content-Type header with application/json. I updated my answer to reflect that.Stressful
Sorry to bother you again, do you know how I can modify the code? doing this returns 200Outpatient
T
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;
                }
            };
        });
Tentation answered 21/5, 2018 at 14:33 Comment(2)
What is “ Pinpoint's solution” ?Gore
There was a solution provided by a user called "Pinpoint" previously, but it seems to be either deleted or the user has renamed.Tentation
D
8

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;
                }
Dann answered 19/10, 2020 at 23:45 Comment(0)
C
3

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;

                }
            }
Copious answered 18/5, 2023 at 11:58 Comment(0)
I
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;                        
                },
Inscription answered 16/3, 2020 at 9:29 Comment(0)
M
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"
        }));
    }
});
Masthead answered 30/9, 2022 at 9:46 Comment(2)
JSON is built in: await context.Response.WriteAsJsonAsync(new Error() { Message = "Token is not valid"});Ledet
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.Anele

© 2022 - 2024 — McMap. All rights reserved.