graphql-dotnet / graphql-dotnet

GraphQL for .NET

Home Page:https://graphql-dotnet.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

@Shane32

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 AuthorizationHandlers 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.