Authorization
See the Authorization project for a more in depth implementation of the following idea. Keep in mind that alongside this project there is a similar Authorization.AspNetCore project specifically for ASP.NET Core apps.
You can write validation rules that will run before a query is executed. You can use this
pattern to check that the user is authenticated or has permissions for a specific field.
This example uses the Metadata
dictionary available on Fields to set permissions per field.
public class MyGraphType : ObjectGraphType
{
public MyGraphType()
{
this.RequirePermission("READ_ONLY");
Field(x => x.Secret).RequirePermission("Admin");
}
}
Validation Rule
public class RequiresAuthValidationRule : IValidationRule
{
public Task<INodeVisitor> ValidateAsync(ValidationContext context)
{
var userContext = context.UserContext as GraphQLUserContext;
var authenticated = userContext.User?.IsAuthenticated() ?? false;
return Task.FromResult(new EnterLeaveListener(_ =>
{
_.Match<Operation>(op =>
{
if (op.OperationType == OperationType.Mutation && !authenticated)
{
context.ReportError(new ValidationError(
context.Document.Source,
"6.1.1", // the rule number of this validation error corresponding to the paragraph number from the official specification
$"Authorization is required to access {op.Name}.",
op) { Code = "auth-required" });
}
});
// this could leak info about hidden fields in error messages
// it would be better to implement a filter on the schema so it
// acts as if they just don't exist vs. an auth denied error
// - filtering the schema is not currently supported
_.Match<Field>(fieldAst =>
{
var fieldDef = context.TypeInfo.GetFieldDef();
if (fieldDef.RequiresPermissions() &&
(!authenticated || !fieldDef.CanAccess(userContext.User.Claims)))
{
context.ReportError(new ValidationError(
context.Document.Source,
"6.1.1", // the rule number of this validation error corresponding to the paragraph number from the official specification
$"You are not authorized to run this query.",
fieldAst) { Code = "auth-required" });
}
});
}));
}
}
Permission Extension Methods
public static class GraphQLExtensions
{
public static readonly string PermissionsKey = "Permissions";
public static bool RequiresPermissions(this IProvideMetadata type)
{
var permissions = type.GetMetadata<IEnumerable<string>>(PermissionsKey, new List<string>());
return permissions.Any();
}
public static bool CanAccess(this IProvideMetadata type, IEnumerable<string> claims)
{
var permissions = type.GetMetadata<IEnumerable<string>>(PermissionsKey, new List<string>());
return permissions.All(x => claims?.Contains(x) ?? false);
}
public static bool HasPermission(this IProvideMetadata type, string permission)
{
var permissions = type.GetMetadata<IEnumerable<string>>(PermissionsKey, new List<string>());
return permissions.Any(x => string.Equals(x, permission));
}
public static void RequirePermission(this IProvideMetadata type, string permission)
{
var permissions = type.GetMetadata<List<string>>(PermissionsKey);
if (permissions == null)
{
permissions = new List<string>();
type.Metadata[PermissionsKey] = permissions;
}
permissions.Add(permission);
}
public static FieldBuilder<TSourceType, TReturnType> RequirePermission<TSourceType, TReturnType>(
this FieldBuilder<TSourceType, TReturnType> builder, string permission)
{
builder.FieldType.RequirePermission(permission);
return builder;
}
}