I am trying to add authorization logic to an ASP.NET core application utilizing graphql-dotnet with your authorization strategy (field level, type level, etc.).
You reference some examples by pasting in a few functions (which I assume are extensions), such as UseGraphQLWithAuth and AddGraphQLWithAuth. I found in the examples where AddGraphQLWithAuth is defined in authorization/src/Harness/GraphQLAuthExtension, but could not locate UseGraphQLWithAuth. Is that something we would add or is it now baked into the graphql-dotnet/server package?
Also, I've been reading issue 502 (graphql-dotnet/graphql-dotnet#502) and wondering if this information is outdated? Is the intention that we define our own middleware code or is that logic now part of graphql-dotnet server? I ask because I've defined my own middleware that works for handling the graphql request, but (for reasons unknown) does not honor the authorization rules.
I really appreciate the graphql-dotnet stuff - I just wish I could find a a current/complete example showing this stuff in action. Any direction appreciated!
Key files below:
*** ghc.GQLAPI.csproj ***
netcoreapp2.1
<PackageReference Include="graphiql" Version="1.1.0" />
<PackageReference Include="GraphQL" Version="2.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.2.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.2.0" />
<PackageReference Include="GraphQL.Authorization" Version="2.0.27"/>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.App"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<!-- <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.Voyager" Version="3.1.0" />
-->
All
All
*** Startup.cs ***
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using AutoMapper;
using GraphQL.Server;
using GraphQL.Http;
using GraphQL;
using GraphQL.Types;
using ghc.GQLAPI.Helpers;
using ghc.Data;
using ghc.Data.Interfaces;
using ghc.Data.Repositories;
using ghc.GQLAPI.Models;
using ghc.GQLAPI.Middleware;
using ghc.Model.Entities;
using GraphQL.Validation;
namespace ghc.GQLAPI
{
public class Startup
{
//TODO: user user secrets to avoid storing salt key in clear text
private IHostingEnvironment _env;
public IConfiguration _configuration { get; }
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
_configuration = configuration;
_env = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
_configuration = builder.Build();
}
// ===DEVELOPMENT
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureDevelopmentServices(IServiceCollection services)
{
// Allow for other classes to access the configuration via dependency injection
services.AddSingleton<IConfiguration>(_configuration);
// *** Configure security/identity ****
// TODO: This is a very weak password strength policy - strengthen for prod
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = false;
opt.Password.RequiredLength = 4;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireUppercase = false;
});
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<GHCOpsDbContext>(); // tell identity system to store security info in the entity framework data store
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
// Add authentication strategy
var key = Encoding.ASCII.GetBytes(_configuration.GetSection("AppSettings:TokenKey").Value);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// Allow us to reference the data seeding process in the configure method below
services.AddTransient<Seed>();
// Specify the DB Context (SqlServer)
services.AddDbContext<GHCOpsDbContext>(options =>
options.UseSqlServer(_configuration["ConnectionStrings:GHCOpsDB"],
b => b.MigrationsAssembly("ghc.Migrations.Development"))
.ConfigureWarnings( warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
);
// Add support for cross origin resource support
services.AddCors();
// Add support for mapping between domain objects and data transfer objects
services.AddAutoMapper();
// BEGIN: GRAPHQL $$$$$$$$$$$$$$$$$$$
// Extension method to allow for GraphQL Authorization logic
services.AddGraphQLAuth(options =>
{
options.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Administrator"));
});
// Add GraphQL services and configure options
services.AddGraphQL(options =>
{
options.EnableMetrics = true;
options.ExposeExceptions = _env.IsDevelopment();
})
.AddWebSockets() // Add required services for web socket support
.AddDataLoader() // Add required services for DataLoader support
.AddUserContextBuilder(httpContext => new GraphQLUserContext { User = httpContext.User });
services.AddSingleton<IDocumentWriter, DocumentWriter>();
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddSingleton<GHCQuery>();
services.AddSingleton<GHCMutation>();
services.AddScoped<IValuesRepository, ValuesRepository>();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IClientsRepository, ClientsRepository>();
services.AddScoped<ICaregiversRepository, CaregiversRepository>();
services.AddScoped<IKeywordsRepository, KeywordsRepository>();
services.AddScoped<INotesRepository, NotesRepository>();
services.AddScoped<IPhotosRepository, PhotosRepository>();
services.AddSingleton<ghc.GQLAPI.Models.ValueType>(); // had to fully qualify since this conflicts with existing asp.net framework type
services.AddSingleton<ClientType>();
services.AddSingleton<UserType>();
services.AddSingleton<CreateUserInputType>();
services.AddSingleton<UpdateUserInputType>();
services.AddSingleton<NoteType>();
services.AddSingleton<PhotoType>();
services.AddSingleton<CaregiverType>();
services.AddSingleton<RecruitingStatusType>();
services.AddSingleton<ServiceStatusType>();
// Do note move these two lines above the graphql services
var sp = services.BuildServiceProvider();
services.AddSingleton<ISchema>(new GHCSchema(new FuncDependencyResolver(type => sp.GetService(type))));
// END: GRAPHQL $$$$$$$$$$$$$$$$$$$$$
// Add Support for Model View Controller Framework
services.AddMvc(options =>
{
var mustBeAuthenticatedPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(mustBeAuthenticatedPolicy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(opt => opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seed seeder)
{
Console.WriteLine("=========================================");
Console.WriteLine("Environment: " + (env.EnvironmentName).ToUpper());
Console.WriteLine("=========================================");
// Ensures migrations have been run or creates the DB from scratch if not found
UpdateDatabase(app);
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else {
// Establish a global exception handler in case we are not in Development mode
app.UseExceptionHandler(builder => {
builder.Run(async context => {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null) {
context.Response.AddApplicationError(error.Error.Message);
await context.Response.WriteAsync(error.Error.Message);
}
});
});
}
// Cross Origin Resource - Order matters - this must come before UseMvc()
// TODO: Tighten up security here
app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());
app.UseWebSockets(); // this is required for websockets support at the ASP.NET CORE level
app.UseGraphQL<ISchema>("/graphqlmw");
app.UseGraphiQl("/graphiql"); // must come before app.UseMvc()!!
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
private static void UpdateDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices
.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
using (var context = serviceScope.ServiceProvider.GetService<GHCOpsDbContext>())
{
context.Database.Migrate();
}
}
}
}
}
*** GraphQLAuthExtensions ***
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using GraphQL.Authorization;
using GraphQL.Validation;
using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using ghc.GQLAPI.Middleware;
namespace ghc.GQLAPI.Helpers
{
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.TryAddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
}
*** ValueType.cs with Authorization specs for a simple type***
using ghc.Model.Entities;
using GraphQL.Authorization;
using GraphQL.Types;
namespace ghc.GQLAPI.Models
{
public class ValueType : ObjectGraphType<Value>
{
public ValueType()
{
// this.AuthorizeWith("AdminPolicy"); // this can be used to protect the entire type
Name = "Value";
Field(x => x.Id).Description("The Value Id");
Field(x => x.Created).Description("Date & Time item was created");
Field<StringGraphType>("Name", "This is the name").AuthorizeWith("AdminPolicy");
}
}
}
*** GraphQLMiddleware.cs - currently not using since I could not get the code to honor authorization rules ***
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using GraphQL.Types;
using Microsoft.AspNetCore.Authorization;
using Newtonsoft.Json.Linq;
using ghc.GQLAPI.Helpers;
using System.Net;
namespace ghc.GQLAPI.Middleware
{
// DDD: This middleware is redundant with the graphqlcontroller, not sure if I should use this code or the controller.
public class GraphQLMiddleware
{
private readonly RequestDelegate _next;
private readonly GraphQLSettings _settings;
private readonly IDocumentExecuter _executor;
private readonly IDocumentWriter _writer;
public GraphQLMiddleware(RequestDelegate next, IDocumentWriter writer, IDocumentExecuter executor, GraphQLSettings settings = null)
{
System.Diagnostics.Debugger.Break();
Console.WriteLine("===================================== INITIALIZING GQL middleware =====================================");
_next = next;
_writer = writer;
_executor = executor;
_settings = settings;
}
public async Task InvokeAsync(HttpContext httpContext, ISchema schema)
{
Console.WriteLine("======== Calling GQL middleware");
if (!IsGraphQLRequest(httpContext))
{
await _next(httpContext);
return;
}
await ExecuteAsync(httpContext, schema);
}
private async Task ExecuteAsync(HttpContext httpContext, ISchema schema)
{
var request = Deserialize<GraphQLRequest>(httpContext.Request.Body);
if (request.Query == null) { throw new ArgumentNullException(nameof(request.Query)); }
var response = await _executor.ExecuteAsync(options =>
{
options.Schema = schema;
options.Query = request.Query;
options.OperationName = request.OperationName;
options.Inputs = request.Variables.ToInputs();
options.UserContext = _settings.BuildUserContext?.Invoke(httpContext);
}).ConfigureAwait(false);
if (response.Errors?.Count > 0)
{
var errors = WriteErrors(response);
ExecutionResult executionResult = new ExecutionResult { Errors = errors };
await WriteResponseAsync(httpContext, executionResult, (int)HttpStatusCode.BadRequest);
}
else
{
await WriteResponseAsync(httpContext, response, (int)HttpStatusCode.OK);
}
}
private bool IsGraphQLRequest(HttpContext context)
{
return context.Request.Path.StartsWithSegments(_settings.Path) && string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase);
}
private async Task WriteResponseAsync(HttpContext httpContext, ExecutionResult result, int httpStatusCode)
{
var json = new DocumentWriter(indent: true).Write(result);
httpContext.Response.StatusCode = httpStatusCode;
httpContext.Response.ContentType = _settings.ResponseContentType;
await httpContext.Response.WriteAsync(json);
}
private ExecutionErrors WriteErrors(ExecutionResult result)
{
var errors = new ExecutionErrors();
foreach (var error in result.Errors)
{
var ex = new ExecutionError(error.Message);
if (error.InnerException != null)
{
ex = new ExecutionError(error.Message, error.InnerException);
}
errors.Add(ex);
}
return errors;
}
public static T Deserialize<T>(Stream s)
{
using (var reader = new StreamReader(s))
using (var jsonReader = new JsonTextReader(reader))
{
var ser = new JsonSerializer();
return ser.Deserialize<T>(jsonReader);
}
}
private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
{
var json = _writer.Write(result);
context.Response.ContentType = "application/json";
context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
await context.Response.WriteAsync(json);
}
}
public class GraphQLRequest
{
public string Query { get; set; }
public string OperationName { get; set; }
public JObject Variables { get; set; }
}
}