How to get GraphQL query in AuthorizationHandler at graphql dotnet 6?
bannarisoftwares opened this issue · comments
I am using graphql-dotnet in dotnet 6, Need to get argument from the query/mutation for authentication. In AuthorizationHandler how to extract the values from query.
This is a sample query
{
formQuery {
form(id: 1, organisationId: "string") {
id
name
organisationId
}
}
}
How to parse formId
and organisationId
from query/mutation?
I want to check organisationId from token and organisationId from query/mutation are same or not.
This is my implementation
public class ValidOrganisationHandler : AuthorizationHandler<ValidOrganisationRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ValidOrganisationHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ValidOrganisationRequirement requirement)
{
if (_httpContextAccessor.HttpContext != null)
{
// How to get value from graphql query/mutation
if (/*parsed value*/)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else
{
context.Fail();
}
context.Succeed(requirement);
// return Task.CompletedTask;
}
Authorization is handled by server project and generic auth project. This repo knows almost nothing about authorization. I suggest to read through readme and samples of both projects to understand design. Server project has special AuthorizationValidationRule
as an entry point into its auth pipeline.
Nevertheless if you want access to the structure of graphql request (AST) you can parse it manually from http request. See GraphQLDocumentBuilder
class. Having parsed AST you may inspect it in any way you need.
@Shane32 Please leave comments if any. Now I'm returning to work in the OSS after a long break, there were a lot of things over the past time. For now it is rather difficult for me to remember everything necessary to make good advice here, especially about auth.
Thank for reply @sungam3r
Hi @sungam3r I am not able to find the example for policy based authorization which based on parsing graphql query/ mutation. Can you please share the example?
Which GraphQL.NET NuGet packages and versions are you using?
Which GraphQL.NET NuGet packages and versions are you using?
I am using dotnet 6 with
GraphQL => 7.5.0
GraphQL.Authorization => 7.0.0
GraphQL.SystemTextJson => 7.5.0
GraphQL.Server.Transports.AspNetCore => 7.4.0
GraphQL.NewtonsoftJson => 7.5.0
GraphQL.MicrosoftDI => 7.5.0
GraphQL.Server.Ui.Altair => 7.4.0
I think you may remove GraphQL.Authorization
since your app is ASP.NET Core based one. GraphQL.Server.Transports.AspNetCore
contains all needed code for auth.
Hi @sungam3r I am not able to find the example for policy based authorization which based on parsing graphql query/ mutation. Can you please share the example?
I was talking only about how to get access to arguments (or other AST parts) of incoming graphql query. AuthorizationHandler
from "vanilla" ASP.NET Core may be not good place to bind with graphq auth. Did you see Samples.Authorization
project? The core thing to understand here - graphql auth infrastructure is independent from "vanilla" ASP.NET Core auth infrastructure and AuthorizationHandler
s are executed first, before AuthorizationValidationRule
that provides additional context about parsed AST.
I think you do not need ValidOrganisationHandler
at all. Custom validation rule may be what you really need. But again, I have no time to dive into now, sorry.
I suggest code like this:
class MyValidationRule : IValidationRule
{
private static readonly AsyncLocal<string?> _queryAuthorizationId = new();
public static string? QueryAuthorizationId => _queryAuthorizationId.Value;
// important not to have this as an async function or else the AsyncLocal won't work as intended
public ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
{
var finder = new OrgIdFinder
{
Variables = context.Variables,
};
// note: do not await here or AsyncLocal won't work as intended -- but it's not async anyway
#pragma warning disable CA2012 // Use ValueTasks correctly
finder.VisitAsync(context.Document, default).GetAwaiter().GetResult();
#pragma warning restore CA2012 // Use ValueTasks correctly
if (finder.Valid && finder.QueryAuthorizationId != null)
{
_queryAuthorizationId.Value = finder.QueryAuthorizationId;
}
return default;
}
}
public class OrgIdFinder : ASTVisitor<OrgIdFinder.Context>
{
public string? QueryAuthorizationId { get; set; }
public bool Valid { get; set; } = true;
public Inputs? Variables { get; set; }
public struct Context : IASTVisitorContext
{
public CancellationToken CancellationToken => default;
}
protected override ValueTask VisitArgumentAsync(GraphQLArgument argument, Context context)
{
if (argument.Name.Value == "orgId")
{
// obtain the orgId from the argument
string? orgId = null;
if (argument.Value is GraphQLStringValue stringValue)
{
orgId = stringValue.Value.ToString();
}
else if (argument.Value is GraphQLVariable variableValue)
{
if (Variables != null && Variables.TryGetValue(variableValue.Name.Value.ToString(), out var value) && value is string str)
{
orgId = str;
}
}
// if the orgId is null or does not match a previous orgId, invalidate the query
if (orgId == null || (QueryAuthorizationId != null && orgId != QueryAuthorizationId))
{
Valid = false;
}
else
{
QueryAuthorizationId = orgId;
}
}
return base.VisitArgumentAsync(argument, context);
}
}
Then add the new validation rule BEFORE the authorization validation rule. Finally, use the static MyValidationRule.QueryAuthorizationId
property within your authorization requirement code. Since the data is stored in an AsyncLocal
, there will be no concurrency issues with accessing the same static variable from multiple threads.
This is just one way to perform the task. Here is another way, using the authorization validation rule from the server repo:
public class MyAuthorizationRule : AuthorizationValidationRule
{
public override async ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
{
var finder = new OrgIdFinder
{
Variables = context.Variables,
};
await finder.VisitAsync(context.Document, default);
// validate finder.QueryAuthorizationId against the current user's token
bool matches = false;
if (matches)
{
return await base.ValidateAsync(context);
}
else
{
context.ReportError(new ValidationError("OrgId does not match current user"));
return null; // no need to run any other policy checks
}
}
}
Or without the ASP.NET authorization policy checks, maybe like this:
public class MyAuthorizationRule : IValidationRule
{
// inject IHttpContextAccessor ?
public async ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
{
var finder = new OrgIdFinder
{
Variables = context.Variables,
};
await finder.VisitAsync(context.Document, default);
var visitor = new MyAuthorizationVisitor(finder.QueryAuthorizationId);
return await visitor.ValidateSchemaAsync(context) ? visitor : null;
}
}
public class MyAuthorizationVisitor : AuthorizationVisitorBase
{
private readonly string? _orgId;
public MyAuthorizationVisitor(string? orgId) // pass in HttpContext ?
{
_orgId = orgId;
}
protected override bool IsAuthenticated => true;
protected override bool IsInRole(string role) => false;
protected override ValueTask<AuthorizationResult> AuthorizeAsync(string policy)
{
// perform policy checks here, comparing the httpcontext orgid against _orgId which is the orgId from the query
}
}
Please comment if you have further questions and I'll reopen the issue.