graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Is there a way to protect all graphql operations by authorization except introspection (global authorization) ?

Open bohdan-kolomiiets opened this issue 3 years ago • 4 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

Cannot find a way to make introspection queries available for anonymours users with global authorization.

I would expect to find an way to specify exclusion for introspection.

I saw similar issue 5056, but I didn't understand why it hadn't pull attention and had been marked as stale, therefore I decided to open this issue.

Steps to reproduce

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 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.MapGraphQLHttp("/graphql").RequireAuthorization();
    endpoints.MapGraphQLSchema("/graphql/schema");
    endpoints.MapBananaCakePop("/graphql/ui");

});
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
{
    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. Open http://localhost:5014/graphql/ui

You will see that introspection query to http://localhost:5014/graphql is failed with http status code 401.

query introspection_phase_1 {
  schema: __type(name: "__Schema") {
    name
    fields {
      name
    }
  }
  directive: __type(name: "__Directive") {
    name
    fields {
      name
    }
  }
}

Relevant log output

No response

Additional Context?

No response

Product

Hot Chocolate

Version

12.14.0

bohdan-kolomiiets avatar Sep 25 '22 13:09 bohdan-kolomiiets

Hey there,

sorry for the late answer. I will do a video on how to set this up properly, there are a couple of questions around this.

For instance... How to secure banana cake pop ... How to secure the introspection with specific policies or how to ensure that this is available for everyone.

We will have a look at the projects we are working on personally and draw from that some recipes.

michaelstaib avatar Oct 30 '22 10:10 michaelstaib

Any update on this? It is now impossible to work with Hot Chocolate + Strawberry Shake with auth configured.

tomasz-soltysik avatar Mar 20 '23 15:03 tomasz-soltysik

I've managed to create a workaround to fix this using .NET Authorization policy: AuthenticatedRequirement .cs

public class AuthenticatedRequirement : IAuthorizationRequirement
{
}

AuthenticatedHandler.cs

public class AuthenticatedHandler(IHttpContextAccessor _httpContextAccessor) : AuthorizationHandler<AuthenticatedRequirement>
{
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AuthenticatedRequirement requirement)
    {
        _httpContextAccessor = _httpContextAccessor ?? throw new ArgumentNullException(nameof(_httpContextAccessor));

        // Is authenticated
        if (_httpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
        {
            context.Succeed(requirement);
        }

        // Is introspection query
        var requestBody = await GetRequestBody();
        var isIntrospectionQuery = requestBody.Contains("IntrospectionQuery");

        if (isIntrospectionQuery)
        {
            context.Succeed(requirement);
        }
    }

    private async Task<string> GetRequestBody()
    {
        var request = _httpContextAccessor.HttpContext.Request;
        request.EnableBuffering();
        var contentLength = Convert.ToInt32(request.ContentLength);
        var buffer = new byte[contentLength];
        await request.Body.ReadAsync(buffer);
        request.Body.Seek(0, SeekOrigin.Begin);
        return Encoding.UTF8.GetString(buffer);
    }
}

And register to use the policy in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
  ...

  services.AddAuthentication(configuration, securityOptions.Authentication);
  services.AddSingleton<IAuthorizationHandler, AuthenticatedHandler>();

  ...
}
public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
  ...

  app.UseEndpoints(endpoints => endpoints
    .MapGraphQLHttp()
    .RequireAuthorization(a =>
    {
        a.Requirements.Add(new AuthenticatedRequirement());
    })
    .WithOptions(new GraphQLHttpOptions()
    {
        AllowedGetOperations = AllowedGetOperations.Query
    }));

  ...
}

oskrabanek avatar Dec 11 '23 15:12 oskrabanek

@oskrabanek that doesn't look very secure. It seems like anyone can now bypass authentication in your system by adding a comment like # IntrospectionQuery somewhere to the query/mutation.

huysentruitw avatar Feb 16 '24 12:02 huysentruitw