graphql-platform
graphql-platform copied to clipboard
Is there a way to protect all graphql operations by authorization except introspection (global authorization) ?
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
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.
Any update on this? It is now impossible to work with Hot Chocolate + Strawberry Shake with auth configured.
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 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.