Introduction:

GraphQL.Validation.IValidationRule . By implementing IValidationRule we have to implement our own custom rules for validating queries. So we can implement our own custom logic for authorization. IValidationRule is the perfect way of implementing authorization because these rules always get executed prior to the query execution.



Here we are going to implement a sample of GraphQL API protecting it by creating claims-based authorization. To know more about GraphQL API Authorization can be done by implementing. By implementing IValidationRule we have to implement our own custom rules for validating queries. So we can implement our own custom logic for authorization. IValidationRule is the perfect way of implementing authorization because these rules always get executed prior to the query execution.Here we are going to implement a sample of GraphQL API protecting it by creating claims-based authorization. To know more about GrapQL API Integration In Asp.Net Core Application Click Here.

Identity Server4 Token Based Authentication: In this sample, we are going to use token-based authentication by IdentityServer 4. If you want you can use any other authentication type like cookie authentication or OAuth2.0 or Microsoft Login Identity. Click here for Identity Server4 Sample Source Code Dotnet Core Web API Verify IdentityServer4 Authentication Token: Let's create a Dotnet Core Web API project, where we are going to configure and protect GraphQL API.



Note:- Following steps are related verifying the IdentityServer4 authentication token, you can skip the following configurations if you are using another login types.



Add the following NuGet for Bearer Token verification

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 3.1.0



Now configure JwtBearer token services

Startup.cs:(ConfigureServices method) Add the following NuGet for Bearer Token verificationNow configure JwtBearer token services

services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:5001"; options.Audience = "graphQLApi"; });

Startup.cs:(Configure method)

app.UseAuthentication();

Configure DbContext And Repositories To Access Data:

Let's quickly add DbContext and Repository entities to access the data from the database.

Models/Cake.cs:

public class Cake { public int Id { get; set; } public string Name { get; set; } public decimal Cost { get; set; } }

Data/BakeryContext.cs:

using Microsoft.EntityFrameworkCore; using Test.GraphQL.MyCoreAPI.Models; namespace Test.GraphQL.MyCoreAPI.Data { public class BakeryContext:DbContext { public BakeryContext(DbContextOptions<BakeryContext> options) : base(options) { } public DbSet<Cake> Cake { get; set; } } }

Startup.cs(ConfigureServices method):

services.AddDbContext>BakeryContext>(options => options.UseSqlServer(Configuration.GetConnectionString("BakeryContext")));

Repos/CakeRepository.cs:

public class CakeRepository : ICakeRepository { private readonly BakeryContext _bakeryContext; public CakeRepository(BakeryContext bakeryContext) { _bakeryContext = bakeryContext; } public List>Cake< GetCakes() { return _bakeryContext.Cake.ToList(); } }

Repos/ICakeRepository.cs:

public interface ICakeRepository { List<Cake> GetCakes(); }

Startup.cs(ConfigureServices method):

services.AddScoped<ICakeRepository, CakeRepository>();

GraphQL NuGet:

Install following NuGet to configure GraphQL API

Install-Package GraphQL -Version 2.4.0

ObjectGraphType OR ObjectGraphType<T>:

ObjectGraphType is one of the main building blocks in GraphQL API. In dotnet, every table will be represented by a class. These POCO classes can't be understood by GraphQL API, so for each and every class in c#, we need to create classes that inherit ObjectGraphType(these will be understood by GraphQL API). The class inherits ObjectGraphType need to register all the fields in its constructor because only registered fields will be served by the GraphQL API.

GraphQLModels/CakeType:

using GraphQL.Types; using Test.GraphQL.MyCoreAPI.Models; namespace Test.GraphQL.MyCoreAPI.GraphQLModel { public class CakeType : ObjectGraphType<Cake> { public CakeType() { Field(_ => _.Id); Field(_ => _.Name); Field(_ => _.Cost); } } }

GraphQLModels/RootQueryType.cs:

using GraphQL.Types; using Test.GraphQL.MyCoreAPI.Repos; namespace Test.GraphQL.MyCoreAPI.GraphQLModel { public class RootQueryType : ObjectGraphType { public RootQueryType(ICakeRepository cakeRepository) { Field<ListGraphType<CakeType>>("allCakes", resolve: context => { return cakeRepository.GetCakes(); }); } } }

Startup.cs(ConfigureServices method):

services.AddScoped<RootQueryType>(); services.AddScoped<CakeType>();

GraphQL Schema:

GraphQL Schema is an execution point for any select query or mutation query(query to update the data in database)

GraphQLModel/RootSchema:

using GraphQL; using GraphQL.Types; namespace Test.GraphQL.MyCoreAPI.GraphQLModel { public class RootSchema : Schema, ISchema { public RootSchema(IDependencyResolver resolver) : base(resolver) { Query = resolver.Resolve<RootQueryType>(); } } }

Startup.cs(ConfigureServices method):

services.AddScoped<IDependencyResolver>(_ => new FuncDependencyResolver(_.GetRequiredService)); services.AddScoped<ISchema, RootSchema>();

Create An GraphQL EndPoint:

Models/GraphQLQueryDto.cs:

public class GraphQLQueryDto { public string Query { get; set; } }

Controller/GraphQLController:

using System.Linq; using System.Threading.Tasks; using GraphQL; using GraphQL.Types; using Microsoft.AspNetCore.Mvc; using Test.GraphQL.MyCoreAPI.Models; namespace Test.GraphQL.MyCoreAPI.Controllers { [Route("graphql")] public class GraphQLController : Controller { private readonly ISchema _schema; private readonly IDocumentExecuter _executer; public GraphQLController(ISchema schema, IDocumentExecuter executer) { _schema = schema; _executer = executer; } [HttpPost] public async Task Post([FromBody] GraphQLQueryDto query) { var result = await _executer.ExecuteAsync(_ => { _.Schema = _schema; _.Query = query.Query; }).ConfigureAwait(false); if (result.Errors?.Count > 0) { return Problem(detail: result.Errors.Select(_ => _.Message).FirstOrDefault(), statusCode: 500); } return Ok(result.Data); } } }

Startup.cs(ConfigureServices):

services.AddSingleton<IDocumentExecuter, DocumentExecuter>();

GraphL Test Query:

query { allCakes { id name } }

Implement IValidationRule To Allow Authenticated User:

Now to write validation rules we need to implement IValidationRule interface as below

Helper/AuthValidationRule:

using System.Security.Claims; using GraphQL.Validation; namespace Test.GraphQL.MyCoreAPI.Helper { public class AuthValidationRule : IValidationRule { public INodeVisitor Validate(ValidationContext context) { var userContext = context.UserContext as ClaimsPrincipal; var authenticated = userContext?.Identity?.IsAuthenticated ?? false; return new EnterLeaveListener(_ => { if (!authenticated) { context.ReportError(new ValidationError( context.OriginalQuery, "authentication-required", "Api can accessed by only autherized user" )); } }); } } }

Startup.cs(ConfigureServices):

services.AddSingleton<IValidationRule, AuthValidationRule>();

Controller/GraphQLController.cs:(Inject IValidationRule)

private readonly IValidationRule _validationRule; public GraphQLController( IValidationRule validationRule) { _validationRule = validationRule; }

Controller/GraphQLController.cs:(assign IvalidationRule to IDocumentExecuter):

var result = await _executer.ExecuteAsync(_ => { _.ValidationRules = new List<IValidationRule> { _validationRule }; }).ConfigureAwait(false);

Controller/GraphQLController.cs:(inject httpContext where user context can access):

private readonly IHttpContextAccessor _httpContextAccessor; public GraphQLController( IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } [HttpPost] public async Task<IActionResult> Post([FromBody] GraphQLQueryDto query) { // display purpose existing code hidden var result = await _executer.ExecuteAsync(_ => { _.UserContext = _httpContextAccessor.HttpContext.User; }).ConfigureAwait(false); }

Startup.cs:

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

Operation Level Authentication:

In GraphQL API using IValidationRule able to setup authentication rules based on "GraphQL.Language.AST.Operation". The two main GraphQL operations like 'Query Operation' represent API data fetching and 'Mutation Operation' represents API posting data.





Now to enable user authentication mandatory while fetching data update validation rule as below

Helper/AuthValidationRule:(authentication while fetching data)

return new EnterLeaveListener(_ => { _.Match<Operation>(op => { if (op.OperationType == OperationType.Query && !authenticated) { context.ReportError(new ValidationError( context.OriginalQuery, "auth-required", $"Authorization is required to fetch data", op)); } }); });

Claim-based Authorization At Field:

Claim-based authorization at fields in root Query or root Mutation is the best approach in GraphQL.

Now we need to register fields with their respective claims if needed. Let's recall our RootQuery field which is looks as below.

Field<ListGraphType<CakeType>>("allCakes", resolve: context => { return cakeRepository.GetCakes(); });

IProviderMetaData(GraphQL Library Interface):

namespace GraphQL.Types { public interface IProvideMetadata { IDictionary<string, object> Metadata { get; } TType GetMetadata<TType>(string key, TType defaultValue = default); bool HasMetadata(string key); } }

Extens/PermissionExtention.cs:

public static class PermissionExtenstion { public static readonly string PermissionKey = "permission"; public static void AddPermissions(this IProvideMetadata type, string claim) { var permissions = type.GetMetadata<List<string>>(PermissionKey); if (permissions == null) { permissions = new List<string>(); } permissions.Add(claim); type.Metadata[PermissionKey] = permissions; } }

GraphQLModel/RootQueryType(Update field with claim):

Field<ListGraphType<CakeType>>("allCakes", resolve: context => { return cakeRepository.GetCakes(); }).AddPermissions("super admin");

Extens/PermissionExtentsion.cs:(add below method)

public static bool HasClaimsMatched(this IProvideMetadata type, IEnumerable<string> claimes) { var permissions = type.GetMetadata<IEnumerable<string>>(PermissionKey, new List<string> { }); return permissions.Any(x => claimes.Contains(x)); }

Helper/AuthValidationRule.cs:

public class AuthValidationRule : IValidationRule { public INodeVisitor Validate(ValidationContext context) { var userContext = context.UserContext as ClaimsPrincipal; var authenticated = userContext?.Identity?.IsAuthenticated ?? false; return new EnterLeaveListener(_ => { _.Match<Field>(fieldAst => { var fieldDef = context.TypeInfo.GetFieldDef(); var claims = userContext.Claims.Select(_ => _.Value).ToList(); if ( (!authenticated || !fieldDef.HasClaimsMatched(claims))) { context.ReportError(new ValidationError( context.OriginalQuery, "auth-required", $"You are not authorized to run this query.", fieldAst)); } }); }); } }

Extens/PermissionExtension.cs:

public static bool AnyPermissions(this IProvideMetadata type) { var permissions = type.GetMetadata<IEnumerable<string>>(PermissionKey, new List<string> { }); return permissions.Any(); }

Helper/AuthValidationRule.cs:

_.Match<Field>(fieldAst => { var fieldDef = context.TypeInfo.GetFieldDef(); var claims = userContext.Claims.Select(_ => _.Value).ToList(); if (fieldDef.AnyPermissions() && (!authenticated || !fieldDef.HasClaimsMatched(claims))) { context.ReportError(new ValidationError( context.OriginalQuery, "auth-required", $"You are not authorized to run this query.", fieldAst)); } });

Wrapping Up:

Hopefully, this article will help to implement GraphQL API authorization in the Dotnet core. I will love to have your feedback, suggestions and better techniques in the comments section.

Refer:

Follow Me: