diff --git a/src/SchemaFirstAuthorization/Example/Example.csproj b/src/SchemaFirstAuthorization/Example/Example.csproj
new file mode 100644
index 0000000..910a43d
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Example.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
diff --git a/src/SchemaFirstAuthorization/Example/Extensions.cs b/src/SchemaFirstAuthorization/Example/Extensions.cs
new file mode 100644
index 0000000..0a095c8
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Extensions.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Example
+{
+ public static class Extensions
+ {
+ public static List WithAttribute(this Assembly assembly)
+ where T : Attribute
+ {
+ return assembly.GetTypes().Where(x => x.GetCustomAttributes().Length > 0).ToList();
+ }
+
+ public static T[] GetCustomAttributes(this Type type)
+ where T : Attribute
+ {
+ return type.GetCustomAttributes(typeof(T), true).OfType().ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SchemaFirstAuthorization/Example/GraphQL.cs b/src/SchemaFirstAuthorization/Example/GraphQL.cs
new file mode 100644
index 0000000..d3cd1cd
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/GraphQL.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.Http;
+
+namespace Example
+{
+ public class GraphQLUserContext : Dictionary
+ {
+ public ClaimsPrincipal User { get; set; }
+ }
+
+ public class GraphQLRequest
+ {
+ [JsonPropertyName("operationName")]
+ public string OperationName { get; set; }
+
+ [JsonPropertyName("query")]
+ public string Query { get; set; }
+
+ [JsonPropertyName("variables")]
+ public GraphQL.Inputs Variables { get; set; }
+ }
+
+ public class GraphQLSettings
+ {
+ public PathString Path { get; set; } = "/api/graphql";
+
+ public Func> BuildUserContext { get; set; }
+
+ public bool EnableMetrics { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/SchemaFirstAuthorization/Example/GraphQLMiddleware.cs b/src/SchemaFirstAuthorization/Example/GraphQLMiddleware.cs
new file mode 100644
index 0000000..c0ef01d
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/GraphQLMiddleware.cs
@@ -0,0 +1,86 @@
+using GraphQL;
+using GraphQL.Authorization;
+using GraphQL.SystemTextJson;
+using GraphQL.Types;
+using GraphQL.Validation;
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Example
+{
+ public class GraphQLMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly GraphQLSettings _settings;
+ private readonly IDocumentExecuter _executer;
+ private readonly IDocumentWriter _writer;
+ private readonly JsonSerializerOptions _serializerOptions;
+
+ public GraphQLMiddleware(
+ RequestDelegate next,
+ GraphQLSettings settings,
+ IDocumentExecuter executer,
+ IDocumentWriter writer)
+ {
+ _next = next;
+ _settings = settings;
+ _executer = executer;
+ _writer = writer;
+
+ _serializerOptions = new JsonSerializerOptions();
+ _serializerOptions.Converters.Add(new InputsConverter());
+ }
+
+ public async Task Invoke(HttpContext context, ISchema schema, IEnumerable rules)
+ {
+ if (!IsGraphQLRequest(context))
+ {
+ await _next(context);
+ return;
+ }
+
+ await ExecuteAsync(context, schema, rules);
+ }
+
+ private bool IsGraphQLRequest(HttpContext context)
+ {
+ return context.Request.Path.StartsWithSegments(_settings.Path)
+ && string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private async Task ExecuteAsync(HttpContext context, ISchema schema, IEnumerable rules)
+ {
+ var request = await JsonSerializer.DeserializeAsync(context.Request.BodyReader.AsStream(), _serializerOptions);
+
+ var result = await _executer.ExecuteAsync(_ =>
+ {
+ _.Schema = schema;
+ _.Query = request?.Query;
+ _.OperationName = request?.OperationName;
+ _.Inputs = request?.Variables;
+ _.UserContext = _settings.BuildUserContext?.Invoke(context);
+ _.ValidationRules = DocumentValidator.CoreRules.Concat(rules);
+ _.EnableMetrics = _settings.EnableMetrics;
+ _.UnhandledExceptionDelegate = context =>
+ {
+ System.Diagnostics.Debug.WriteLine(context.OriginalException.Message);
+ };
+ });
+
+ await WriteResponseAsync(context, result);
+ }
+
+ private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
+ {
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
+
+ await _writer.WriteAsync(context.Response.Body, result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SchemaFirstAuthorization/Example/Program.cs b/src/SchemaFirstAuthorization/Example/Program.cs
new file mode 100644
index 0000000..8bf8bc2
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Example
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/src/SchemaFirstAuthorization/Example/Properties/launchSettings.json b/src/SchemaFirstAuthorization/Example/Properties/launchSettings.json
new file mode 100644
index 0000000..96b0ddc
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:39369",
+ "sslPort": 44370
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Example": {
+ "commandName": "Project",
+ "dotnetRunMessages": "true",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/SchemaFirstAuthorization/Example/Requirements.cs b/src/SchemaFirstAuthorization/Example/Requirements.cs
new file mode 100644
index 0000000..c29927e
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Requirements.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using GraphQL.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Example
+{
+ ///
+ /// This represents a custom Authorization Requirement. These can be used
+ /// to get data from an external source (such as a database) or check
+ /// information that isn't directly available in the ClaimsPrincipal.
+ ///
+ /// For example, you can use IHttpContextAccessor to check data on the HttpContext.
+ ///
+ public class UserIsJoeRequirement : IAuthorizationRequirement
+ {
+ private readonly IHttpContextAccessor _accessor;
+
+ public UserIsJoeRequirement(IHttpContextAccessor accessor)
+ {
+ _accessor = accessor;
+ }
+
+ public Task Authorize(AuthorizationContext context)
+ {
+ // this is the same as context.User
+ if (_accessor.HttpContext.User.Identity?.Name != "Joe")
+ {
+ context.ReportError("User is not Joe!");
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/SchemaFirstAuthorization/Example/Resolvers.cs b/src/SchemaFirstAuthorization/Example/Resolvers.cs
new file mode 100644
index 0000000..58530e7
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Resolvers.cs
@@ -0,0 +1,42 @@
+using System;
+using GraphQL;
+
+namespace Example
+{
+ public class GraphTypeMetadataAttribute : Attribute
+ {
+ public GraphTypeMetadataAttribute(string typeDef)
+ {
+ TypeDef = typeDef;
+ }
+
+ public string TypeDef { get; }
+ }
+
+ [GraphTypeMetadata(@"
+ type User {
+ id: ID
+ name: String
+ }
+ ")]
+ public class User
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ [GraphQLMetadata("Query")]
+ [GraphTypeMetadata(@"
+ extend type Query {
+ viewer: User
+ }
+ ")]
+ public class QueryType
+ {
+ [GraphQLAuthorize("CustomRequirement")]
+ public User Viewer()
+ {
+ return new User { Id = 1, Name = "Quinn" };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SchemaFirstAuthorization/Example/Startup.cs b/src/SchemaFirstAuthorization/Example/Startup.cs
new file mode 100644
index 0000000..d585f01
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/Startup.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Linq;
+using GraphQL;
+using GraphQL.Authorization;
+using GraphQL.Server.Ui.Playground;
+using GraphQL.Types;
+using GraphQL.Validation;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Example
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthorization();
+ services.AddHttpContextAccessor();
+
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddTransient(s =>
+ {
+ var authSettings = new AuthorizationSettings();
+ // add a policy that checks claims on the Authenticated User
+ authSettings.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Admin"));
+
+ // add a policy which uses a custom AuthorizationRequirement
+ authSettings.AddPolicy("CustomRequirement", p => p.AddRequirement(s.GetRequiredService()));
+ return authSettings;
+ });
+
+ services.AddSingleton(s =>
+ {
+ // get all types in assembly that have the GraphTypeMetadataAttribute attribute
+ // use the schema-first type definitions to build the schema
+ var types = typeof(QueryType).Assembly.WithAttribute();
+ var typeDefs = types
+ .SelectMany(x => x.GetCustomAttributes())
+ .Select(x => x.TypeDef)
+ .ToArray();
+
+ var schemaFirst = string.Join(Environment.NewLine, typeDefs);
+
+ return Schema.For(schemaFirst, _ =>
+ {
+ foreach (var type in types)
+ {
+ _.Types.Include(type);
+ }
+ });
+ });
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseRouting();
+ app.UseAuthorization();
+
+ app.UseGraphQLPlayground(new PlaygroundOptions { GraphQLEndPoint = "/api/graphql" });
+
+ app.UseMiddleware(new GraphQLSettings
+ {
+ Path = "/api/graphql",
+ BuildUserContext = ctx => new GraphQLUserContext
+ {
+ User = ctx.User
+ },
+ EnableMetrics = true
+ });
+ }
+ }
+}
diff --git a/src/SchemaFirstAuthorization/Example/appsettings.Development.json b/src/SchemaFirstAuthorization/Example/appsettings.Development.json
new file mode 100644
index 0000000..dba68eb
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/src/SchemaFirstAuthorization/Example/appsettings.json b/src/SchemaFirstAuthorization/Example/appsettings.json
new file mode 100644
index 0000000..81ff877
--- /dev/null
+++ b/src/SchemaFirstAuthorization/Example/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/SchemaFirstAuthorization/README.md b/src/SchemaFirstAuthorization/README.md
new file mode 100644
index 0000000..f72ed49
--- /dev/null
+++ b/src/SchemaFirstAuthorization/README.md
@@ -0,0 +1,8 @@
+# ASP.NET Core Schema First GraphQL Example
+
+This example demonstrates a Schema First approach with authorizating access to graph types.
+
+```
+> ./run.sh
+> browse to http://localhost:3000/ui/playground
+```
diff --git a/src/SchemaFirstAuthorization/run.sh b/src/SchemaFirstAuthorization/run.sh
new file mode 100755
index 0000000..efff7b1
--- /dev/null
+++ b/src/SchemaFirstAuthorization/run.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cd Example
+dotnet restore
+dotnet run