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.

Request with unsuccessful authorization returns 500 status code

See original GitHub issue

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When authentication middleware fails to issue authentication ticket but field requires user to be authenticated, 500 http status code returned,

As far as I read, according to GraphQL style, server should return 200 in most cases or 500 if things are really bad (but I didn’t find any official documentation about this). Also, graphql-dotnet returns 200 http status code in case of authorization errors.

Therefore, I expected hotchocolate to return 200 (better this) or at least 401/403 status codes.

Steps to reproduce

I have tried two different variants. First one is authentication with JWT Second one is authentication with custom authentication handler.

JWT authentication

1. Build code

HotChocolateAuthDemo.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="HotChocolate.AspNetCore" Version="12.14.0" />
    <PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="12.14.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.9" />
  </ItemGroup>
</Project>

Properties/launchSettings.json

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "HotChocolateAuthDemo": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "http://localhost:5014",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Program.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using HotChocolate.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGraphQLServer().AddQueryType<Query>().AddMutationType<Mutation>().AddAuthorization();
builder.Services.AddSingleton<NotesRepository>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.Zero,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecretKey"))
    };
});
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapGraphQL();
});
app.Run();

public class Mutation
{
    public string ObtainJwt(string userId)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes("MySuperSecretKey");
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new("UserId", userId),
            }),
            IssuedAt = DateTime.UtcNow,
            Expires = DateTime.UtcNow.AddMinutes(2),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        var jwt = tokenHandler.WriteToken(token);
        return jwt;
    }
}

public class Query
{
    [Authorize]
    public NoteViewModel[] GetMyNotes(
        [Service] NotesRepository repository,
        ClaimsPrincipal user)
    {
        var userId = user.FindFirstValue("UserId");
        
        var viewModels = repository.Value
            .Where(note => note.OwnerId == userId)
            .Select(note => note.ToViewModel())
            .ToArray();
        return viewModels;
    }
}
[ObjectType("Note")]
public record NoteViewModel(string Id, string Title, string Body);


public class NotesRepository
{
    public readonly NoteDto[] Value = new[]
    {
        new NoteDto(Id: "1", OwnerId: "1", Title: "user 1 note 1 title", Body: "user 1 note 1 body"),
        new NoteDto(Id: "1", OwnerId: "2", Title: "user 2 note 1 title", Body: "user 2 note 1 body")
    };
}
public record NoteDto(string Id, string OwnerId, string Title, string Body)
{
    public NoteViewModel ToViewModel() => new NoteViewModel(Id, Title, Body);
}

2. Obtain token

mutation {
  obtainJwt(userId: "1")
}

3. Execute query with token passed in Authorization header

Authorization: Bearer {jwt}

{
  myNotes {
    title
  }
}

Custom authentication handler

1. Build code

HotChocolateAuthDemo.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="HotChocolate.AspNetCore" Version="12.14.0" />
    <PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="12.14.0" />
  </ItemGroup>
</Project>

Properties/launchSettings.json

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "HotChocolateAuthDemo": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "http://localhost:5014",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Program.cs

using System.Security.Claims;
using System.Text.Encodings.Web;
using HotChocolate.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGraphQLServer().AddQueryType<Query>().AddAuthorization();
builder.Services.AddSingleton<NotesRepository>();
builder.Services.AddAuthentication(defaultScheme: "UserId")
    .AddScheme<AuthenticationSchemeOptions, UserIdAuthHandler>(authenticationScheme: "UserId", _ => { });
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapGraphQL();
});
app.Run();

public class UserIdAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public UserIdAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    { }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue("UserId", out var userId))
        {
            return Task.FromResult(AuthenticateResult.NoResult());
        }
        var ticket = new AuthenticationTicket(
            authenticationScheme: "UserId",
            principal: new ClaimsPrincipal(
                identity: new ClaimsIdentity(
                    authenticationType: "UserId",
                    claims: new []{ new Claim("UserId", userId) }
                )));
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

[ObjectType]
public class Query
{
    [Authorize]
    public NoteViewModel[] GetMyNotes(
        [Service] NotesRepository repository,
        ClaimsPrincipal user)
    {
        var userId = user.FindFirstValue("UserId");
        
        var viewModels = repository.Value
            .Where(note => note.OwnerId == userId)
            .Select(note => note.ToViewModel())
            .ToArray();
        return viewModels;
    }
}
[ObjectType("Note")]
public record NoteViewModel(string Id, string Title, string Body);


public class NotesRepository
{
    public readonly NoteDto[] Value = new[]
    {
        new NoteDto(Id: "1", OwnerId: "1", Title: "user 1 note 1 title", Body: "user 1 note 1 body"),
        new NoteDto(Id: "1", OwnerId: "2", Title: "user 2 note 1 title", Body: "user 2 note 1 body")
    };
}
public record NoteDto(string Id, string OwnerId, string Title, string Body)
{
    public NoteViewModel ToViewModel() => new NoteViewModel(Id, Title, Body);
}

2. Execute query passed UserId header

UserId: 1

{
  myNotes {
    title
  }
}

Relevant log output

No response

Additional Context?

No response

Product

Hot Chocolate

Version

12.14.0

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:5
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

2reactions
tr4ckscommented, Nov 8, 2022

@mac2000 To keep the data when they are present (in the case of a 200 code) it is possible to do this: I think it’s more compliant with the way Hot Chocolate and GraphQL work in general.

public class ApplicationHttpResultSerializer : DefaultHttpResultSerializer
{
    public override HttpStatusCode GetStatusCode(IExecutionResult result)
    {
        if (result is not IQueryResult { Data: null, Errors.Count: > 0 } queryResult)
        {
            return base.GetStatusCode(result);
        }
        
        return queryResult.Errors switch
        {
            var errors when
                errors.Any(x => x.Code == "AUTH_NOT_AUTHENTICATED") =>
                HttpStatusCode.Unauthorized,
            var errors when
                errors.Any(x => x.Code == "AUTH_NOT_AUTHORIZED") =>
                HttpStatusCode.Forbidden,
            _ => base.GetStatusCode(result)
        };
    }
}
0reactions
KrankenWargencommented, Aug 10, 2023

Has this been resoluted ? we have the same issue

Read more comments on GitHub >

github_iconTop Results From Across the Web

Getting a 500 status code when performing a sign-up request
I am getting an Auth0Exception with message: Request failed with status code 500: Unknown exception whenever I execute a SignUpRequest.
Read more >
500 Internal Server Error - HTTP - MDN Web Docs - Mozilla
This error response is a generic "catch-all" response. Usually, this indicates the server cannot find a better 5xx error code to response.
Read more >
Axios POST request fails with error status code 500
I'm trying to send a POST request locally with a username and password in the body through Axios. ... HOWEVER using Postman with...
Read more >
What is “HTTP 500 Internal Server Error” and How to Fix It?
Error 500, also known as the Internal Server Error, is a common HTTP status code that indicates an issue on the web server's...
Read more >
Fixing a 500 internal server error response
A 500 Internal Server Error is an HTTP status code that indicates that the server encountered an unexpected error while processing the request....
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