server icon indicating copy to clipboard operation
server copied to clipboard

Is there a way to change the status code of the authorization extension from 400 to 401?

Open nichovski opened this issue 2 years ago • 10 comments

Hi guys,

It is very strange when my HTTP response codes are not corresponding to the error I want to return to the front end. How can I change that?

I've tried overriding the GraphQLMiddleware, but it is triggered only when my query doesn't have errors.

Thank you!

nichovski avatar Jan 09 '23 07:01 nichovski

Override the WriteJsonResponseAsync method, inspect to see if there are any AccessDeniedError instances within the Errors property of the response, and if so call the base method with the response code of 401 instead of the statusCode passed to the method.

Shane32 avatar Jan 09 '23 13:01 Shane32

This answer is applicable when using GraphQL.NET Server 7 to host your endpoint.

Shane32 avatar Jan 09 '23 13:01 Shane32

At the bottom of this section there is a sample demonstrating how to override methods of GraphQLHttpMiddleware and use the derived instance

https://github.com/graphql-dotnet/server#graphqlhttpmiddleware

Shane32 avatar Jan 09 '23 13:01 Shane32

The same technique can be used to return any status code for any validation error.

Note that for transport-level errors, including authentication errors, the status code will already be set properly by the GraphQLHttpMiddleware. But validation errors, such as those raised by the authentication validation rule, always return by default either 200 or 400 based on the ValidationErrorsReturnBadRequest option.

Shane32 avatar Jan 09 '23 13:01 Shane32

Thank you, I will try that out.

nichovski avatar Jan 09 '23 13:01 nichovski

Other option you have is to add your own ExecutionError to the IResolveFieldContext and there is a "Data" column that is a dictionary you can use to stuff additional error information like that. Then pull from the ExecuteAsync result "Errors" field.

OpenSpacesAndPlaces avatar Jan 10 '23 14:01 OpenSpacesAndPlaces

@nichovski Could you provide expected response and actual response to demonstrate where you want 400 instead of 401?

sungam3r avatar Jan 12 '23 17:01 sungam3r

@Shane32 thanks for the help. I've tried multiple ways to solve that, but nothing worked as I wanted. Instead of returning 401 on the GraphQL level, I wrote GraphQLHTTPMiddleware for catching errors.

I'm sharing my solution here for everyone interested.

  public class GraphQLHTTPMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        public GraphQLHTTPMiddleware(RequestDelegate next,
                                                ILoggerFactory loggerFactory)
        {
            _next = next;
            _logger = loggerFactory
                      .CreateLogger<GraphQLHTTPMiddleware>();
        }
        public async Task Invoke(HttpContext context)
        {
           
            Stream originalBody = context.Response.Body;

            try
            {
                using var memStream = new MemoryStream();
                context.Response.Body = memStream;

                await _next.Invoke(context);

                if (context.Request.Path.StartsWithSegments("/graphql"))
                {
                    if (context.Request.Method == "POST")
                    {
                        if (context.Response.StatusCode == 400)
                        {
                            memStream.Position = 0;
                            string responseBody = new StreamReader(memStream).ReadToEnd();
                            context.Response.StatusCode = GetHttpErrorCode(responseBody, context);
                        }
                    }
                }

                memStream.Position = 0;
                await memStream.CopyToAsync(originalBody);

            }
            finally
            {
                context.Response.Body = originalBody;
            }
        }

        private int GetHttpErrorCode(string jsonString, HttpContext context)
        {
            var error = JsonSerializer.Deserialize<GraphQLErrorResult>(jsonString);
            if (error.Errors.Any(x => x.Extensions.Code == "ACCESS_DENIED"))
            {
                return 401;
            }
            return context.Response.StatusCode;


        }
    }

nichovski avatar Jan 23 '23 08:01 nichovski

Did you try the code designed as I suggested? It should perform identically without the need to copy the stream or deserialize the response. It does require GraphQL.NET Server 7.0 or later. See below:

// within Program.cs:
app.UseGraphQL<MyGraphQLHttpMiddleware<ISchema>>("/graphql", new GraphQLHttpMiddlewareOptions());

// custom middleware
public class MyGraphQLHttpMiddleware<T> : GraphQLHttpMiddleware<T>
    where T : ISchema
{
    public MyGraphQLHttpMiddleware(RequestDelegate next, IGraphQLTextSerializer serializer,
        IDocumentExecuter<T> documentExecuter, IServiceScopeFactory serviceScopeFactory,
        GraphQLHttpMiddlewareOptions options, IHostApplicationLifetime hostApplicationLifetime)
        : base(next, serializer, documentExecuter, serviceScopeFactory, options, hostApplicationLifetime)
    {
    }

    protected override Task WriteJsonResponseAsync<TResult>(HttpContext context, HttpStatusCode httpStatusCode, TResult result)
    {
        // place code here to change the status code as desired
        if ((result as ExecutionResult)?.Errors?.Any(x => x.Code == "ACCESS_DENIED") ?? false)
            httpStatusCode = HttpStatusCode.Unauthorized;

        return base.WriteJsonResponseAsync(context, httpStatusCode, result);
    }
}

Shane32 avatar Jan 23 '23 10:01 Shane32

Yeah, I've tried and it works great. Thanks a lot for your solution. We are still deciding what solution we should choose.

nichovski avatar Jan 26 '23 12:01 nichovski

Version 8 will return more appropriate status codes from the authorization validation rule in certain cases

Shane32 avatar Aug 09 '24 02:08 Shane32