Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 3
Welcome to part three in this series, in which we're building a real-world GraphQL-driven application using Angular, ASP.NET Core, and IdentityServer.
In the last post, we progressed the solution by creating the skeleton for our Angular application and then set up some necessary infrastructure and components to provide functionality for creating and authenticating users.
Finally, we hooked it up to our backend ASP.NET Core API and IdentityServer using a couple of new services with some help from the oidc-client
library to provide OpenID Connect and OAuth2 protocol support.
If you'd like to get fully up to speed before proceeding with this portion of the guide, I'd suggest you go and check out the last post before digging into this one.
In this post, we'll build out more of the backend by implementing a GraphQL API complete with authentication using Joe Mcbride's fantastic GraphQL .NET library.
We'll write tests to help guide and validate the development.
Using the ASP.NET Core Test Host and EF Core In-Memory Database Provider, we can stand up an in-memory instance of the GraphQL API Web stack. The ability to host the GraphQL server in a test harness like this will enable us to write tests that quickly validate the functionality and integration of its major components independent of any browser, network, or database server.
Posts in This Series
- Part 1: Building an authentication and identity server with IdentityServer
- Part 2: Angular app foundation with user signup and login features
- Part 3: Implementing an ASP.NET Core GraphQL API with authorization using GraphQL .NET 👈 You're here!
- Part 4: Integrating Angular with a backend GraphQL API using Apollo Client
Development Environment
As of January 2020, this guide was created using the most recent versions of:
- .NET Core 3.1
- Visual Studio Code
- Visual Studio 2019 Professional for Windows
- SQL Server Express 2016 LocalDB
- Node.js with npm
- Angular CLI
Running the Solution
See the repository's readme for the steps required to build and run the demo application.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
GraphQL API Solution Design
Before we jump into the implementation, let's map out a high-level design to provide some structure and help identify the various layers in our solution along with their primary responsibility.
Our solution will consist of the following three layers:
-
FullStackJobs.GraphQL.Infrastructure
The infrastructure layer will house our data access provided by Entity Framework Core, our GraphQL engine provided by GraphQL .NET and any other external dependencies or facilities we might require. -
FullStackJobs.GraphQL.Core
The core layer will encapsulate our domain and business logic. In here, we'll place the entities to represent the key actors in our application's domain like jobs, applicants, and employers. We want to keep this layer as pure as possible by not introducing any external libraries or frameworks. We'll use interfaces to provide abstractions for access to external dependencies and things like repository classes that rely on the database down in the Infrastructure layer. -
FullStackJobs.GraphQL.Api
The api layer is the entry point for our GraphQL web service, which will be powered by an ASP.NET Core Web API project. This layer is responsible for fielding http requests from the Angular client and calling on the other layers to do the heavy-lifting of producing a response for the requested GraphQL resources. This layer will also provide authorization for incoming requests by calling on ourAuthServer
to perform access token validation. 🔐
With the high-level layers and responsibilities in place, we're ready to cut some code to get our GraphQL service off the ground. 🚀
FullStackJobs.GraphQL solution
Similar to the AuthServer we'll create a new solution to house the GraphQL service and its projects and then include them in the main FullStackJobs.sln solution file. This setup will allow us to run and debug both solutions at the same time within Visual Studio.
To accomplish this, I created a new blank solution and then added an empty .NET Standard class library project, which will be the home of the Core domain layer.
Back in the root FullStackJobs.sln
solution file I added a new solution folder for the GraphQL projects and added the new Core
project where we'll being implementing our application's domain entities.
A Basic Domain Model
We're intentionally keeping our demo application as simple as possible so we can focus on the overall architectural design and integration of its components. For our purposes, we want to avoid getting too bogged down in the type of complicated requirements and nuances we typically encounter when writing software in the real world.
However, we still need to define some basic entities and relationships for our domain including Job
, Employer
and Applicant
that will allow us to fluently express the domain of our system in code and in this guide.
These new DDD-styled entity classes will also be mapped to our database using Entity Framework Core as well as represented by types in our GraphQL schema.
Although simple, these classes will underpin our data, application, and GraphQL models. In a more extensive, more complex system, this likely wouldn't be the case, but again in the name of simplicity, it will work fine for our purposes.
Within the Core
project, I added a couple of new entity classes to represent an Applicant
and Employer
. If you'll recall from the last post, these match up with the Role dropdown field options we placed on the signup component.
Infrastructure Layer
As we covered above in the solution overview, similar to the infrasctructure layer we created for the AuthServer
in the first part of this series, we need a similar layer within our GraphQL solution. For now, this project will mainly contain any code and packages relating to data access and the GraphQL engine. These will be the dependencies on Entity Framework Core and GraphQL .NET.
I created a new .NET Core Class Library project and used NuGet to fetch the required Entity Framework Core packages we need to get started.
Data Layer
The first responsibility we'll implement within the infrastructure layer is the management of the application's data. Using Entity Framework Core, we can directly map the entity classes we created previously in the Core
project to tables in the database.
To accomplish this, I set up the AppDbContext to create the bridge between the domain entity classes and the database.
To provide design-time support for the EF Core tools to generate migrations from inside the Infrasctructure
class library project I added AppDbContextFactory. For more information on specifying design-time options for creating a DbContext
please check out the docs.
Migrations
We're now ready to create an EF Core migration that will update the database schema with our application's data model as defined by the AppDbContext
.
Within the root of the FullStackJobs.GraphQL.Infrastructure
project folder, I used the command-line tools to generate the migration file and update the database.
\> dotnet ef migrations add initial
\> dotnet ef database update
Running the database update
command applies the migration by creating new tables in the Sql Server database for each of the entity classes.
Implementing GraphQL for .NET Core
With our entity model and data layer in place, we have the backing required to begin designing and building out the GraphQL service within our solution.
I've decided to place the GraphQL bits inside of the Infrastructure project so to get started I brought in the GraphQL and GraphQL.Authorization packages with nuget. I created a GraphQL folder in the root of the Infrastructure
project to house all of our GraphQL related code.
Defining a Schema
Regardless of your programming environment, the first step in designing any GraphQL service starts with a schema to define the shape of the data clients can request.
The schema is essentially a hierarchy of types and their fields that get populated and returned by the GraphQL service. Additionally, it specifies the query and mutation operations clients can request from the server to fetch and modify data.
GraphQL .NET offers two ways to define a schema. The schema-first approach involves designing the schema upfront using the GraphQL schema language. While the code-first approach allows the schema to be defined programmatically in a strongly-typed yet more verbose fashion using GraphType
objects. We'll build our GraphQL schema using the code-first method.
Our core schema object is FullStackJobsSchema
, which inherits from the GraphQL .NET type Schema
.
This class maps its Query
and Mutation
properties to two additional classes in FullStackJobsQuery
and FullStackJobsMutation
.
namespace FullStackJobs.GraphQL.Infrastructure.GraphQL
{
public class FullStackJobsSchema : Schema
{
public FullStackJobsSchema(IServiceProvider services) : base(services)
{
Query = services.GetService(typeof(FullStackJobsQuery)).As<IObjectGraphType>();
Mutation = services.GetService(typeof(FullStackJobsMutation)).As<IObjectGraphType>();
}
}
}
These classes provide the root operation query and mutation types for the schema. We'll explore both in greater detail as we work through implementing the various data operations provided by our GraphQL service.
IServiceProvider
represents a reference to the service container that used to resolve both root types.
This project is using Autofac in place of the out-of-the-box ASP.NET Core service container. Using an Autofac module we can bundle up all of the registrations for the GraphQL types in our schema in a central InfrastructureModule.
In just a bit, we'll see how to register the required GraphQL .NET types and components within the services container using Autofac.
Types
The essential components of any GraphQL schema are the object types it provides, representing the meaningful entities in our application's model.
JobType adds the existing Job
entity to our schema and exposes its properties to define the available fields this type will provide.
The GraphQL .NET library will automatically translate any primitive .NET types to their equivalent GraphQL scalar type. Additionally, the GraphQL .NET project provides specific types to resolve special things like collections and enumerations as used on the Field<ListGraphType<TagType>>
and Field<EnumerationGraphType<Status>>
field definitions.
namespace FullStackJobs.GraphQL.Infrastructure.GraphQL.Types
{
public class JobType : ObjectGraphType<Job>
{
public JobType(ContextServiceLocator contextServiceLocator)
{
Field(x => x.Id);
Field(x => x.Position);
Field(x => x.Company, true);
Field(x => x.Icon);
Field(x => x.Location, true);
Field(x => x.AnnualBaseSalary, true).AuthorizeWith(Policies.Employer);
Field(x => x.Description, true);
Field(x => x.Responsibilities, true);
Field(x => x.Requirements, true);
Field(x => x.ApplicationInstructions, true);
Field<ListGraphType<TagType>>("tags", resolve: context => context.Source.Tags);
Field<EnumerationGraphType<Status>>("status");
Field<StringGraphType>("modified", resolve: context => contextServiceLocator.Humanizer.TimeAgo(context.Source.Modified ?? context.Source.Created));
Field<IntGraphType>("applicantCount", resolve: context => context.Source.JobApplicants.Count);
}
}
}
The true
parameter used on several of the fields mark them as nullable in the schema. By default, the GraphQL .NET library sets them as non-nullable, which can quickly lead to exceptions if the underlying data store contains nulls.
Finally, the .AuthorizeWith(Policies.Employer)
declaration on the AnnualBaseSalary
field is an authorization extension point that restricts access to that specific field to those possessing the Employer role.
We'll explore the steps to secure our schema by adding and configuring user authorization within our GraphQL API using the GraphQL.Authorization
library we installed earlier.
Queries
With JobType
now available in the schema, we can implement the first query operation inside the root query object.
This query fetches a job specified by the id
argument defined for the field using the QueryArgument
passed in the field's arguments
parameter. The client must send this value as part of the request to the GraphQL service.
The resolve
parameter is where the field's Resolver function is defined. In the GraphQL .NET, this is a Func
that supplies a ResolveFieldContext
where argument values are accessible by the custom code we write to help produce the field's data.
In this example, the field data is retrieved from the database using the EF Core JobRepository
call GetSingleBySpec(...)
which grabs the id
value out of the received arguments supplied by the context via context.GetArgument<int>("id", default)
.
Every field in GraphQL must have a corresponding Resolver function to produce the data for that field. But the source of that data could be anything or anywhere: database, other REST API call, cache, etc.
public class FullStackJobsQuery : ObjectGraphType
{
public FullStackJobsQuery(ContextServiceLocator contextServiceLocator)
{
FieldAsync<JobType>("job",
arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
resolve: async context => await contextServiceLocator.JobRepository.GetSingleBySpec(new JobSpecification(j => j.Id == context.GetArgument<int>("id", default))));
// More query operations to come...
}
}
ContextServiceLocator is a helper dependency injected via the contstructor that - as its name might suggest - acts a service locator inside our GraphQL objects to resolve the dependencies they need like JobRepository
.
Graph Type Authorization
The GraphQL.Authorization library provides extension points for authorizing access to the graph types in your schema using a policy-based approach. 🔐
The documentation for this library is a bit sparse currently, but using the instructions in the repository and various issue threads, I was able to piece together the following approach to implement authorization within GraphQL .NET.
Registering Authorization Classes and Policies
To get started, I created a new custom service collection extension method that is responsible for registering the required GraphQL.Authorization classes in the container along with the AuthorizationSettings
class that contains the policies we'd like to use to restrict access to any GraphType
or Field
in our schema.
These registrations are added to the container by calling services.AddGraphQLAuth()
from within the ConfigureServices()
method in the API project's Startup class.
public static class ServiceConfigurationExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services)
{
services.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
authSettings.AddPolicy(Policies.Employer, _ => _.RequireClaim(ClaimTypes.Role, "employer"));
authSettings.AddPolicy(Policies.Applicant, _ => _.RequireClaim(ClaimTypes.Role, "applicant"));
return authSettings;
});
}
}
Policy Configuration using AuthorizationPolicyBuilder
Note the two claim-based policies added to the AuthorizationSettings
singleton.
These policies are defined using ASP.NET Core's AuthorizationPolicyBuilder class which adds custom authorization requirements to the request pipeline.
The two policies we'll be using call the RequireClaim()
method to ensure the Role
claim is present within the current ClaimsPrincipal
and contains a value of either "employer" or "applicant".
When communicating with the API using our authenticated Angular client, the access_token
supplied by IdentityServer gets sent along with each request and includes this claim value as part of its payload. We won't get far enough in this post to see that in action, however, we will look at how to inject this value into the ClaimsPrincipal
during test scenarios using filters in our test harness to make sure the policies behave as expected.
As you can probably guess, we'll restrict access to employer-specific fields using the Employer
policy and vice-versa for the Applicant
one.
This small snippet doesn't seem that exciting at first glance. However, the simplicity and versatility of defining custom policies in this fashion using AuthorizationPolicyBuilder
combined with the ability to apply them to any GraphType
or Field
at any level in the schema's hierarchy is a very powerful and pleasant approach to securing our GraphQL .NET service.
UserContext
The next step was to create a GraphQLUserContext
object that provides the ClaimsPrincipal
containing the actual claim values we just reviewed. This context makes the claims available to the validation rules and policies that get run before the GraphQL .NET engine executes a query.
public class GraphQLUserContext : Dictionary<string, object>, IProvideClaimsPrincipal
{
public ClaimsPrincipal User { get; set; }
}
Apply Policies to Schema GraphTypes and Fields
With these bits in place, we're ready to apply our policies to any GraphType
or Field
we'd like to secure in the schema using the AuthorizeWith(string policy)
extension method.
We've seen one example of this already in the JobType
class where access to the highly-sensitive AnnualBaseSalary
field is being restricted to Employers only.
As we build out the GraphQL service further in the next section, we'll be applying these policies to restrict access to some of the new fields we add.
public class JobType : ObjectGraphType<Job>
{
public JobType(ContextServiceLocator contextServiceLocator)
{
...
Field(x => x.AnnualBaseSalary, true).AuthorizeWith(Policies.Employer);
...
}
}
GraphQL Controller
With most of the essential bits for GraphQL .NET engine setup, we now need to add the entry point in our API project to route incoming requests for GraphQL data to the GraphQL .NET engine.
To acheive this, I copied the code for GraphQLController from the GraphQL .NET samples repository and placed it in a new /Controllers folder in the root of the API project.
This controller contains just one route and action method to handle GraphQL requests POSTed to the /graphql
endpoint route.
I had to make a couple of small modifications by setting two properties on the ExecutionOptions
object related to authorization.
The first was to set the UserContext
property to the GraphQLUserContext
object we created earlier that holds the claim values for the current user request. The second is setting the ValidationRules
property using dependency injection. This property receives the AuthorizationSettings
object containing any policies we registered with the container.
These property assignments provide the claim data and policies required by the GraphQL .NET engine to enforce authorization when executing a query.
[Route("[controller]")]
public class GraphQLController : Controller
{
private readonly IDocumentExecuter _documentExecuter;
private readonly ISchema _schema;
public GraphQLController(ISchema schema, IDocumentExecuter documentExecuter)
{
_schema = schema;
_documentExecuter = documentExecuter;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] GraphQLQuery query, [FromServices] IEnumerable<IValidationRule> validationRules)
{
if (query == null) { throw new ArgumentNullException(nameof(query)); }
var inputs = query.Variables.ToInputs();
var executionOptions = new ExecutionOptions
{
Schema = _schema,
Query = query.Query,
Inputs = inputs,
UserContext = new GraphQLUserContext
{
User = User
},
ValidationRules = validationRules,
#if (DEBUG)
ExposeExceptions = true,
EnableMetrics = true,
#endif
};
var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
return BadRequest(result);
}
return Ok(result);
}
}
Guided by Tests
We've covered a lot of ground in this guide to get the GraphQL service off the ground. 🏃
To recap, we implemented a basic domain model to represent the key actors in our system like Employer
, Applicant
, and Job
.
Next, we added the Entity Framework Core data layer to map those entities to data objects and also set up a repository to provide a convenient way for the GraphQL engine to talk to the database.
Finally, we dove into the GraphQL server implementation by adding the required packages for GraphQL .NET, building up our schema by creating graph fields, types and supporting services, and securing it all by introducing additional code and configuration to enable authorization.
At this point, we could fire everything up and use a tool like GraphiQL in the browser to test the GraphQL service and validate all the moving pieces are working together as expected.
But, long term in a real-world project with this many moving parts, we can't rely on these types of manual methods to flag regressions as the GraphQL service's components are modified or extended over time to support new requirements.
As we saw in part 1 of this series, we can solve this problem using integration tests equipped with the ASP.NET Core Test Host and EF Core In-Memory Provider.
We can combine these frameworks in our tests to provide an in-memory instance of our entire GraphQL API stack with all of its major components running together.
With this test harness, we can construct the same GraphQL requests a client application like our SPA would produce and very quickly execute them against the test instance of our service.
We can also test the authorization policies without involving IdentityServer, physical access_tokens, or interaction with a real browser, network socket, or database server.
To get started, I added a new FullStackJobs.GraphQL.Api.IntegrationTests xUnit test project to the Tests folder and added the Microsoft.AspNetCore.Mvc.Testing
package to it via nuget.
The first test I added is called CanFetchJob
that executes the job
query we created earlier.
[Theory]
[InlineData(1)]
public async Task CanFetchJob(int id)
{
// arrange
var client = GetFactory().CreateClient();
_dbContext.Add(EntityFactory.MakeJob("123", "C# Ninja"));
_dbContext.SaveChanges();
var httpResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "/graphql")
{
Content = new StringContent($@"{{""query"":""query FullStackJobsQuery($id: Int!)
{{
job(id: $id) {{
position
}}
}}"",
""variables"":{{""id"":{id}}},
""operationName"":""FullStackJobsQuery""}}", Encoding.UTF8, "application/json")
});
httpResponse.EnsureSuccessStatusCode();
var content = await httpResponse.Content.ReadAsStringAsync();
Assert.Equal(@"{""data"":{""job"":{""position"":""C# Ninja""}}}", content);
}
Let's break down how this test is set up and executed.
Custom WebApplicationFactory
The very first statement in the arrange
section is doing a few important things.
var client = GetFactory().CreateClient()
The GetFactory()
method lives in a shared IntegrationTestBase class that provides a WebApplicationFactory
to the test context used to create an instance of the in-memory TestServer
.
protected WebApplicationFactory<TStartup> GetFactory(bool isEmployer = false, bool isApplicant = false)
{
return _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddInMemoryDataAccessServices<TDbContext>();
});
builder.ConfigureTestServices(services =>
{
services.AddMvc(options =>
{
if (isEmployer)
{
options.Filters.Add(new EmployerUserFilter());
}
if (isApplicant)
{
options.Filters.Add(new ApplicantUserFilter());
}
});
});
});
}
The factory is pretty straightforward but makes a couple of customizations to the web host to support our integration tests.
The first is configuring the service collection by adding the in-memory provider for Entity Framework Core to prevent the need to connect to the physical SQL Server database from within our tests.
The generic type parameter <TStartup>
the WebApplicationFactory
requires represents the entry point of the application which for us is the Startup class in the Web API project.
It's worth calling this out as our test app's builder.ConfigureServices
callback is executed after the Web API's Startup.ConfigureServices
code gets executed as the factory bootstraps the test host with our app. This execution order means we're effectively overriding the "primary" configuration defined by the main Startup
class.
...
builder.ConfigureServices(services =>
{
services.AddInMemoryDataAccessServices<TDbContext>();
});
...
Faking Authenticated Requests with Action Filters
The next customization helps us support testing the authorization policies we've applied to the GraphQL schema.
The builder.ConfigureTestServices()
callback allows us to optionally add filters into the request pipeline that will inject a user with dummy claim data used to exercise the Applicant' and
Employer` policies.
The optional isEmployer
and isApplicant
bool parameters of the GetFactory()
method allows a test to specify which type of user they would like the client request to mimic. Not specifying one of these arguments will prevent any filter from being added, which is analogous to an unauthenticated request from a real client with no token or claim data present.
builder.ConfigureTestServices(services =>
{
services.AddMvc(options =>
{
if (isEmployer)
{
options.Filters.Add(new EmployerUserFilter());
}
if (isApplicant)
{
options.Filters.Add(new ApplicantUserFilter());
}
});
});
The filter classes assign dummy claim data to the HttpContext.User
including a value for the Role
claim.
These classes along with some other shared testing bits live in the Testing.Support project.
public class EmployerUserFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, "123"),
new Claim(ClaimTypes.Name, "Jon Doe"),
new Claim(ClaimTypes.Email, "employer@fullstackjobs.com"),
new Claim(ClaimTypes.Role, "employer")
}));
await next();
}
}
Seeding the InMemroy Database Context
After the factory setup, the next piece of arrangement the test code performs is to seed the in-memory database with some fake data to support the test.
In this case, we need some data for a single Job
to exist in the database to ensure the GraphQL query we're testing fetches it as expected.
...
_dbContext.Add(EntityFactory.MakeJob("123", "C# Ninja"));
_dbContext.SaveChanges();
...
The in-memory _dbContext
instance is initialized via the constructor before each test is run and released automatically after the test run completes using the Dispose()
method.
...
public GraphQLControllerIntegrationTests(FullStackJobsApplicationFactory<Startup> factory) : base(factory)
{
_dbContext = DbContextFactory.MakeInMemoryProviderDbContext<AppDbContext>(Configuration.InMemoryDatabase);
}
public void Dispose()
{
_dbContext.Database.EnsureDeleted();
_dbContext.Dispose();
}
...
Executing the GraphQL Request
With the test host configured and seed data in place, the next statement prepares and sends the GraphQL request using the HttpClient
instance created by the web application factory at the beginning of the test.
...
var httpResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "/graphql")
{
Content = new StringContent($@"{{""query"":""query FullStackJobsQuery($id: Int!)
{{
job(id: $id) {{
id
position
}}
}}"",
""variables"":{{""id"":{id}}},
""operationName"":""FullStackJobsQuery""}}", Encoding.UTF8, "application/json")
});
The serialized request is submitted using the POST method to the /graphql endpoint route, which gets handled by our GraphQLController
. You can use the debugger to step through the code and take a closer look at how the controller executes the query.
The query has a required, non-nullable id
parameter representing the job we're interested in retrieving. To keep things simple here, we're only asking for the id' and
position` fields in the response.
...
job(id: $id) {{
id
position
}}
Checking the Results
Finally, we check the outcome by making sure we received a successful status code in the response and that the response payload content contains job data matching the one we seeded the database with.
...
httpResponse.EnsureSuccessStatusCode();
var content = await httpResponse.Content.ReadAsStringAsync();
Assert.Equal(@"{""data"":{""job"":{""id"":1,""position"":""C# Ninja""}}}", content);
Testing Authorization
One example where we use an authorization policy to protect a field within the GraphQL schema is restricting access to the AnnualBaseSalary
field on JobType
to Employers only.
public JobType(ContextServiceLocator contextServiceLocator)
{
...
Field(x => x.AnnualBaseSalary, true).AuthorizeWith(Policies.Employer);
}
I set up two tests in GraphQLControllerIntegrationTests specifically to verify the policy assignment is working as expected
The CantFetchAnnualBaseSalaryAsApplicant
test requests the annualBaseSalary
in the context of an Applicant
.
We turn the ApplicantFilter
on in the request pipeline via the isApplicant
parameter in the call to get the factory.
...
// arrange
// acting as an applicant
var client = GetFactory(isApplicant: true).CreateClient();
...
This test expects the request to fail with a HttpStatusCode.BadRequest
status when the underlying authorization fails in the GraphQL .NET engine as it's enforcing the policy to only permit Employers access to the field.
To test that path, I wrote the CanFetchAnnualBaseSalaryAsEmployer
test next, which makes the same request in the context of an Employer
by injecting the EmployerFilter
and its claims into the request pipeline for the test.
...
// arrange
// acting as an employer
var client = GetFactory(isEmployer: true).CreateClient();
...
This test is expected to complete successfully with the annualBaseSalary
field in the response payload.
Testing Authorized Requests with GraphiQL
In addition to automated integration tests, we can use GraphiQL to execute more ad hoc GraphQL queries from within a browser.
Adding GraphiQL support to the project is as simple as installing the GraphiQL middleware package along with a tiny snippet of configuration in the request pipeline within Startup's Configure
method.
...
app.UseGraphiql("/graphiql", options =>
{
options.GraphQlEndpoint = "/graphql";
});
...
At present, there isn't a way to inject request headers and tokens into GraphiQL requests, which means there's no easy way to test the authorization policies work correctly.
The result is an authorization failure sent from the server when it receives a request for a query or mutation that requires authorization.
Inject Access Token with ModHeader
This problem is easy to solve in chrome using the ModHeader extension.
First, create an account and log in then click the Show Access Token link in the top navbar.
This will inject the access token for the currently logged in user into the page body where you can select and copy it onto your clipboard.
Next, fire up the GraphiQL IDE by going to https://localhost:5025/graphiql and then open up the ModHeader extension.
Create a new Authorization
header and paste the token in the value field.
Now, running the same request in GraphiQL with the Authorization
header containing the access token works as expected. ✔️
Completing the GraphQL API
I repeated the steps we covered to add the additional Fields, Types, Query, and Mutation operations the GraphQL service must provide. There's no need to cover them individually as they were derived using the same workflow and testing patterns we covered in detail throughout this post.
If you'd like to explore the additional code go check out the FullStackJobsQuery and FullStackJobsMutation classes.
You can also refer to the integration tests as currently each query and mutation operation has at least one test covering it.
Wrapping Up
In this post, we implemented a basic domain model and then used Entity Framework Core to map its entity classes to the database.
Next, we worked through setting up a GraphQL .NET-powered API by defining a schema to represent our entity models with GraphQL types along with query and mutation operations consuming clients can use to fetch and modify the data.
We also added authorization capabilities to the API using the GraphQL.Authorization
library to configure custom authorization policies. These policies offer a versatile, yet relatively simple way to secure Fields and Operations in the GraphQL schema.
Finally, we wrote integration tests using the ASP.NET Core TestHost
and EF Core InMemory provider to quickly validate all the major components comprising our GraphQL API work together as expected. These tests exercise most of the code in the GraphQL API stack without requiring a physical database, network, or running IdentityServer.
We've accomplished a lot in this post. At this point, we've got a well-structured, testable, and secure GraphQL API in place that is ready to be consumed by a client application. In the next part of this series, we'll do just that by moving back over to the UI to begin integrating the Angular app with our new GraphQL API using the Apollo GraphQL Client for Angular.
Stay tuned and happy programming!
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Related Posts
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 1
Dec 8, 2019
Read moreBuild an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 2
Dec 30, 2019
Read moreBuild an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 4
Mar 19, 2020
Read moreGet notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.