Skip to content

Commit

Permalink
Add authentication [WIP] (#42)
Browse files Browse the repository at this point in the history
* Add authentication

* Add Options

* Add additional tests

* Update tests

* Clean up

* More Clear up

* More Clean up

* Remove thread.sleep

* Waits
  • Loading branch information
josephwoodward authored Mar 6, 2020
1 parent c28f6c7 commit 79dee57
Show file tree
Hide file tree
Showing 18 changed files with 622 additions and 106 deletions.
77 changes: 36 additions & 41 deletions src/GraphiQl/GraphiQlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,75 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;

namespace GraphiQl
{
public static class GraphiQlExtensions
{
private const string DefaultPath = "/graphql";
private const string DefaultGraphQlPath = "/graphql";

public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app)
=> UseGraphiQl(app, DefaultPath);

public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app, string path)
=> UseGraphiQl(app, path, null);
public static IServiceCollection AddGraphiQl(this IServiceCollection services)
=> services.AddGraphiQl(null);

/// <param name="path"></param>
/// <param name="apiPath">In some scenarios it makes sense to specify the API path and file server path independently
/// Examples: hosting in IIS in a virtual application (myapp.com/1.0/...) or hosting API and documentation separately</param>
public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app, string path, string apiPath)
public static IServiceCollection AddGraphiQl(this IServiceCollection services, Action<GraphiQlOptions> configure)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException(nameof(path));
if (configure != null)
{
services.Configure(configure);
}

services.TryAdd(ServiceDescriptor.Transient<IConfigureOptions<GraphiQlOptions>, GraphiQlOptionsSetup>());

return services;
}

if (path.EndsWith("/"))
throw new ArgumentException("GraphiQL path must not end in a slash", nameof(path));
public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IOptions<GraphiQlOptions>>().Value;

var filePath = $"{path}/graphql-path.js";
var uri = !string.IsNullOrWhiteSpace(apiPath) ? apiPath : path;
app.Map(filePath, x => WritePathJavaScript(x, uri));
var filePath = $"{options.GraphiQlPath.TrimEnd('/')}/graphql-path.js";
var graphQlPath = !string.IsNullOrWhiteSpace(options.GraphQlApiPath) ? options.GraphQlApiPath : DefaultGraphQlPath;
app.Map(filePath, x => WritePathJavaScript(x, graphQlPath));

return UseGraphiQlImp(app, x => x.SetPath(path));
return UseGraphiQlImp(app, options);
}

private static IApplicationBuilder UseGraphiQlImp(this IApplicationBuilder app, Action<GraphiQlConfig> setConfig)
private static IApplicationBuilder UseGraphiQlImp(this IApplicationBuilder app, GraphiQlOptions options)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
if (setConfig == null)
throw new ArgumentNullException(nameof(setConfig));

var config = new GraphiQlConfig();
setConfig(config);

var fileServerOptions = new FileServerOptions
{
RequestPath = config.Path,
FileProvider = BuildFileProvider(),
RequestPath = options.GraphiQlPath,
FileProvider = new EmbeddedFileProvider(typeof(GraphiQlExtensions).GetTypeInfo().Assembly, "GraphiQl.assets"),
EnableDefaultFiles = true,
StaticFileOptions = {ContentTypeProvider = new FileExtensionContentTypeProvider()}
};

app.UseMiddleware<GraphiQlMiddleware>();
app.UseFileServer(fileServerOptions);

return app;
}

private static IFileProvider BuildFileProvider()
{
var assembly = typeof(GraphiQlExtensions).GetTypeInfo().Assembly;
var embeddedFileProvider = new EmbeddedFileProvider(assembly, "GraphiQl.assets");

var fileProvider = new CompositeFileProvider(
embeddedFileProvider
);

return fileProvider;
}

private static void WritePathJavaScript(IApplicationBuilder app, string path)
{
private static void WritePathJavaScript(IApplicationBuilder app, string path) =>
app.Run(h =>
{
h.Response.ContentType = "application/javascript";
return h.Response.WriteAsync($"var graphqlPath='{path}';");
});
}

[Obsolete("This overload has been marked as obsolete, please configure via IServiceCollection.AddGraphiQl(..) instead or consult the documentation", true)]
public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app, string path)
=> throw new NotImplementedException();

[Obsolete("This overload has been marked as obsolete, please configure via IServiceCollection.AddGraphiQl(..) instead or consult the documentation", true)]
public static IApplicationBuilder UseGraphiQl(this IApplicationBuilder app, string path, string apiPath)
=> throw new NotImplementedException();
}
}
31 changes: 31 additions & 0 deletions src/GraphiQl/GraphiQlMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace GraphiQl
{
public class GraphiQlMiddleware
{
private readonly RequestDelegate _next;
private readonly GraphiQlOptions _options;

public GraphiQlMiddleware(RequestDelegate next, IOptions<GraphiQlOptions> options)
{
_next = next;
_options = options.Value;
}

public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Equals(_options.GraphiQlPath, StringComparison.OrdinalIgnoreCase)
&& _options.IsAuthenticated != null
&& !await _options.IsAuthenticated.Invoke(context))
{
return;
}

await _next(context);
}
}
}
38 changes: 38 additions & 0 deletions src/GraphiQl/GraphiQlOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace GraphiQl
{
public class GraphiQlOptions
{
public string GraphiQlPath { get; set; }

public string GraphQlApiPath { get; set; }

public Func<HttpContext, Task<bool>> IsAuthenticated { get; set; }

public GraphiQlOptions()
{
GraphiQlPath = "/graphql";
GraphQlApiPath = "/graphql";
}
}

public class GraphiQlOptionsSetup : IConfigureOptions<GraphiQlOptions>
{
public void Configure(GraphiQlOptions options)
{
if (options.GraphiQlPath == null)
{
options.GraphiQlPath = "/graphql";
}

if (options.GraphQlApiPath == null)
{
options.GraphQlApiPath = "/graphql";
}
}
}
}
1 change: 1 addition & 0 deletions tests/GraphiQl.Demo/Controllers/GraphQlController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace GraphiQl.Demo.Controllers
{
[Route(Startup.GraphQlPath)]
[Route(Startup.CustomGraphQlPath)]
public class GraphQlController : Controller
{
[HttpPost]
Expand Down
3 changes: 0 additions & 3 deletions tests/GraphiQl.Demo/GraphiQl.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
Expand Down
17 changes: 7 additions & 10 deletions tests/GraphiQl.Demo/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -11,6 +10,7 @@ namespace GraphiQl.Demo
public class Startup
{
public const string GraphQlPath = "/graphql";
public const string CustomGraphQlPath = "/custom-path";

public Startup(IConfiguration configuration)
{
Expand All @@ -19,26 +19,23 @@ public Startup(IConfiguration configuration)

public IConfiguration Configuration { get; }

public virtual void ConfigureGraphQl(IServiceCollection services)
=> services.AddGraphiQl();

public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.AddNewtonsoftJson(
options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore
);

ConfigureGraphQl(services);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{

/*
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
*/

app.UseGraphiQl(GraphQlPath);
app.UseGraphiQl();
app.UseRouting().UseEndpoints(
routing => routing.MapControllers()
);
Expand Down
73 changes: 73 additions & 0 deletions tests/GraphiQl.Tests/AuthenticationTest/ConfigureOptionsSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GraphiQl.Demo;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;

namespace GraphiQl.Tests.AuthenticationTest
{
public class ConfigureOptionsSetup : SeleniumTest, IAsyncLifetime
{
private readonly IWebHost _host;

public ConfigureOptionsSetup()
{
_host = WebHost.CreateDefaultBuilder()
.ConfigureServices(x => { x.AddTransient<IConfigureOptions<GraphiQlOptions>,GraphiQlTestOptionsSetup>(); })
.UseStartup<Startup>()
.UseKestrel()
.UseUrls("http://*:5001")
.Build();
}

[Fact]
public void RequiresAuthentication()
{
// Arrange + Act
var result = string.Empty;
RunTest(driver =>
{
driver.Navigate().GoToUrl("http://localhost:5001/graphql");

driver.Manage()
.Timeouts()
.ImplicitWait = TimeSpan.FromSeconds(2);

result = driver.PageSource;
});

// Assert
result.ShouldContain("This page requires authentication");
}

public async Task InitializeAsync()
=> await _host.StartAsync().ConfigureAwait(false);

public Task DisposeAsync()
{
_host.Dispose();
return Task.CompletedTask;
}

internal class GraphiQlTestOptionsSetup : IConfigureOptions<GraphiQlOptions>
{
public void Configure(GraphiQlOptions options)
{
options.IsAuthenticated = context =>
{
context.Response.Clear();
context.Response.StatusCode = 400;
context.Response.WriteAsync("This page requires authentication");

return Task.FromResult(false);
};
}
}
}
}
Loading

0 comments on commit 79dee57

Please sign in to comment.