Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 4
Welcome to the final post of multi-part series, where we've explored the design and construction of a GraphQL-driven app built with Angular and ASP.NET Core.
In the last post, we implemented an ASP.NET Core GraphQL server, an underlying data layer with help from Entity Framework Core and the GraphQL schema to define the types and operations that will satisfy the data requirements of our Angular application.
Finally, we secured some regions of the GraphQL API by applying role-based authorization policies to restrict access to several graph types and fields within the schema.
In this post, we'll tie everything together by implementing some real-world features in the demo Angular application.
These features will make authenticated calls to the GraphQL API using the Apollo GraphQL Client for Angular and validate the identity and authorization functions provided by IdentityServer are working as expected across the solution's components.
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
- Part 4: Integrating Angular with a backend GraphQL API using Apollo Client π You're here!
Development Environment
As of March 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.
Recap
In Part 2, we scaffolded a new Angular application then bootstrapped it with Angular Material. We then implemented user signup and login components along with a couple of new services for integrating with the authorization server.
In this post, we'll build on top of the foundation we've established by implementing some basic features specific to the Employer
and Applicant
entities we modeled in the previous post.
For Employers, this will include functionality to create and edit job posts, and for Applicants, we'll add the ability to apply to those postings.
These features are relatively simple on their own. However, they'll demonstrate the required steps and components involved in making authorized requests from Angular to the ASP.NET Core GraphQL API.
Adding Apollo Client to the Angular Project
To GraphQL-ify the Angular app, we need to add the required packages to support Apollo Client which will do all the low-level work in handling the interactions with our GraphQL server.
I used npm
to install the required packages from the command prompt.
Spa> npm install --save apollo-angular \
apollo-angular-link-http \
apollo-link \
apollo-client \
apollo-cache-inmemory \
graphql-tag \
graphql
Alternatively, this library now supports Angular Schematics so that you can install it with a single command.
Spa> ng add apollo-angular
Creating and Configuring the Apollo Client
With the dependencies in place, we're ready to create and configure the Apollo Client.
The apollo-angular
package provides the ApolloModule
and APOLLO_OPTIONS
token to configure Apollo Client.
As per the docs, we could simply add the Apollo configuration directly to app.module.ts
however in the spirit of keeping app.module
as clean as possible I placed the configuration in a separate module we'll import into the main app.module
.
The module uses the ApolloLink
interface, which acts as middleware to modify the GraphQL request and response pipeline.
Apollo Links are chainable "units" that we can snap together in a composable fashion to define how our GraphQL client handles each request.
I defined two custom links:
cleanTypeName
- Removes unwanted fields containing type info from the response.authLink
- Enables us to make authorized GraphQL requests by adding the access token to the request'sAuthorization
header. The token is fetched from theauthorizationHeaderValue
property we added as part of theAuthService
in part 2.
...
// Workaround "Unrecognized input fields '__typename' for type..." error
// https://stackoverflow.com/questions/47211778/cleaning-unwanted-fields-from-graphql-responses/51380645#51380645
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
const omitTypename = (key: string, value: any) => (key === '__typename' ? undefined : value);
operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
}
return forward(operation).map((data) => {
return data;
});
});
const authLink = new ApolloLink((operation, forward) => {
// Get the authentication token from our authService if it exists
const token = authService.authorizationHeaderValue;
// Use the setContext method to set the HTTP headers.
operation.setContext({
headers: {
'Authorization': token ? token : ''
}
});
The custom links are chained together along with the HttpLink
that tells Apollo Client to use an http connection to talk to our GraphQL Server.
The configuration for the cache
setting uses an instance of InMemoryCache
, which is the default cache implementation for Apollo Client.
...
return {
link: cleanTypeName.concat(authLink.concat(httpLink.create({ uri: configService.graphqlURI }))),
cache: new InMemoryCache(),
};
The last step to make the configuration provider module globally available to GraphQL requests throughout the application is to import it into our root app.module.
With all this hooked up, we've got Apollo Client ready to make authorized requests to our GraphQL API. π
Query and Mutation Services
Apollo provides Query
and Mutation
APIs that can be used to create services that execute your GraphQL queries.
I created a new query to fetch an employer's jobs by extending the Query
class. We define the operation and the shape of its result by setting the document
property.
This query's fields are defined by the jobSummaryFields
fragment. Fragments are a handy way to consistently share fields between your queries.
The Injectable
decorator makes the query available to the dependency injection system and available as a dependency in other classes like any regular Angular service.
@Injectable({
providedIn: 'root',
})
export class EmployerJobsGQL extends Query<JobListResponse> {
document = gql`
query FullStackJobsQuery
{
employerJobs {
...jobSummaryFields
}
},
${jobSummaryFieldsFragment}
`;
}
On the server-side, we're calling the employerJobs
query we implemented previously in the FullStackJobsQuery class.
The resolve
function for the query is pretty straightforward.
It extracts the user's id from the incoming request via context.GetUserId()
. This function lives in GraphQLExtensions class which fetches the id from the sub
claim of the JWT access token. By default, the JWT authentication handler in .NET will map the sub
claim of a JWT access token to the System.Security.Claims.ClaimTypes.NameIdentifier
.
The user id is used to create a specification object that is handed List()
method of the JobRepository
. This query specification pattern is a useful way to enforce some consistency and reusability around the logic we use to query the database.
FieldAsync<ListGraphType<JobSummaryType>>("employerJobs",
resolve: async context =>
{
// Extract the user id from the name claim to fetch the target employer's jobs
var jobs = await contextServiceLocator.JobRepository.List(new JobSpecification(j => j.Employer.Id == context.GetUserId()));
return jobs.OrderByDescending(j => j.Modified);
});
Mutations are set up and function identically to queries.
I created a new one by extending the Mutation
class and defined its operation by setting the document
property.
For now, we only require the id
field in the response.
Mutation operations typically expect input arguments to pass back to the GraphQL API. This operation doesn't, but we'll get to an example of this shortly when we add a new mutation to handle updating jobs.
@Injectable({
providedIn: 'root',
})
export class CreateJobGQL extends Mutation {
document = gql`
mutation ($input: CreateJobInput) {
createJob(input: {}){
id
}
}
`;
}
This operation maps to the createJob
mutation in our GraphQL schema as defined in the FullStackJobsMutation class.
The resolve
function for this operation relies on the JobRepository
to add a new Job
entity with seed values for the Position
and Icon
properties.
The AuthorizeWith()
extension protects the operation, so only authorized Employers can execute it.
FieldAsync<JobType>("createJob",
resolve: async context => await contextServiceLocator.JobRepository.Add(Job.Build(context.GetUserId(),
"Untitled Position", $"icon-{new Random().Next(1, 4)}.png"))).AuthorizeWith(Policies.Employer);
Employer Module
With Apollo Client configured and a couple of operations implemented, we're ready to start adding UI components that will use these services to fetch data with GraphQL.
We'll create a new module to organize the components we need to support the Employer
use cases.
These will consist of a simple view that lists the employer's current job postings and a second, more complex module with a few child components used to edit a job's various details.
To get started, I used the Angular CLI to generate the new module in the root app
folder.
app> ng g module employer
Jobs Component
The first component to add inside this module is one that simply displays the jobs belonging to the logged-in employer.
I used the CLI to create new a jobs component in the employer module folder.
The component fetches its data using the employerJobsGQL
query service we just created.
The fetch()
method returns an Observable
that provides the response data used to set the jobs
property, which is bound in the UI template.
Note the fetchPolicy
argument; this tells Apollo Client to ignore the cache for this query and always use the network to go back to the server.
If the call fails, the returned error is displayed using Angular Material's MatDialog
service.
...
ngOnInit() {
this.busy = true;
this.employerJobsGQL.fetch(null, { fetchPolicy: 'network-only' }).pipe(finalize(() => {
this.busy = false;
})).subscribe(result => {
this.jobs = result.data['employerJobs'];
}, (error: any) => {
this.dialog.open(DialogComponent, {
width: '250px',
data: { title: 'Oops!', message: error }
});
});
}
This component can also create new jobs by calling the createJobGQL
mutation service.
Similar to the query we just implemented, we can subscribe to the observable returned by the service's mutate()
method to obtain the data about the newly created job from the response.
From there, we pass the id
of the new job to the job management component where we can further edit it; we'll explore this component next.
...
createJob() {
this.createJobGQL.mutate().subscribe((result: { data: { [x: string]: any; }; }) => {
let job = result.data['createJob'];
this.router.navigate(['/employer', 'job', job.id, 'manage', 'basics']);
});
}
Finally, the component template uses a mat-nav-list
component with a binding to the jobs
property to display a navigation link to the job management component for each item in the array.
<section fxLayout="row wrap" fxLayoutAlign="center center">
<mat-card fxFlex="850px" fxFlex.xs="100%">
<h1>My Job Postings</h1>
<p *ngIf="jobs?.length === 0">Looks like you haven't posted any jobs yet - post one now!</p>
<div fxLayout="row" fxLayoutAlign="center">
<a mat-raised-button (click)="createJob()" [disabled]="busy">
<mat-icon>post_add</mat-icon>
Post a new job
</a>
</div>
<mat-nav-list>
<a mat-list-item *ngFor="let job of jobs" [routerLink]="['/employer', 'job', job.id, 'manage', 'basics']">
{{ job.position + ' [' + job.status +' ~' + job.modified +']' }}
</a>
</mat-nav-list>
<div class="spinner-container" *ngIf="busy">
<mat-spinner color="accent" [diameter]="40"></mat-spinner>
</div>
</mat-card>
</section>
Job Management Module
The second piece of functionality we'll add is a more sophisticated bit of UI to support editing a job.
This feature consists of several individual child components to encapsulate each section of data, i.e., basic info, description, etc. We'll build it in its very own module to keep everything nicely organized together.
I used the CLI to create a new job-management submodule within the employer
folder.
employer> ng g module job-management
Base Component Using Inheritance
TypeScript is incredible; it brings object-oriented and statically typed concepts into the dynamic, sometimes unruly world of JavaScript.
One object-oriented concept we can leverage for this feature is inheritance. Typescript uses syntactic sugar to mimic class and inheritance behavior.
To share code across all the module's component classes, I created a BaseJobManagementComponent
using the abstract
keyword.
Marking the class abstract
enforces the same rules as C# in that this class may not be instantiated directly but can provide implementations for its members that are available to derived classes.
export abstract class BaseJobManagementComponent {
protected router: Router;
protected updateJobGQL: UpdateJobGQL;
protected employerJobGQL: EmployerJobGQL;
public busy: Boolean;
public job: Job;
constructor() {
// https://devblogs.microsoft.com/premier-developer/angular-how-to-simplify-components-with-typescript-inheritance/
// Manually retrieve the dependencies from the injector so that subclass ctors contain no dependencies that must be passed in from child
const injector = AppInjector.getInjector();
this.router = injector.get(Router);
this.updateJobGQL = injector.get(UpdateJobGQL);
this.employerJobGQL = injector.get(EmployerJobGQL);
}
}
Note the use of injector
which is a reference to Angular's application-wide injector. We use it to get
instances of a few services the child components will require.
Using this mechanism to wire up the shared services in the base class removes the need for each subclass to specify constructor arguments for them. This pattern helps simplify the child component classes and keeps things nice and DRY.
Check out the original guide on simplifying components with typescript inheritance which fully illustrates the power of this feature.
Implementing the Child Components
With the base component in place, we're ready to tackle the individual sub-components that make up the job-management
feature.
Root Component
The root component provides the UI layout container and a router-outlet
where child views are rendered. This component also makes the only call within the entire feature to fetch the job data using the employerJobGQL
query service.
...
ngOnInit() {
if (!this.job) {
this.busy = true;
this.employerJobGQL.fetch({ id: +this.activatedRoute.snapshot.paramMap.get("id") }, { fetchPolicy: 'network-only' }).pipe(finalize(() => {
this.busy = false;
})).subscribe(result => {
this.job = result.data.job;
});
}
}
Basics Component
The basics component is really simple and just presents form fields to collect the job's basic details.
Description Component
The description component presents a couple of advanced fields using the CKEditor rich text component for Angular.
To install the editor, I added the required packages to the project using npm
.
Spa> npm install --save @ckeditor/ckeditor5-angular
Spa> npm install --save @ckeditor/ckeditor5-build-classic
Next, I added CKEditorModule
to the job-management module and imported the editor build in the description component.
The last step was adding the ckeditor
component to the template markup with bindings to the associated job
property.
<ckeditor [editor]="descriptionEditor" [ngModel]="job?.description" (ngModelChange)="job.description = $event" name="description"></ckeditor>
Application Component
The application component is identical to description
.
Tags Component
The tags component is a simple widget to attach tags to the posting.
Publish Component
Finally, the publish component is where the status for the post is set.
This component contains a save()
method, which calls the updateJobGQL
mutation service we set up earlier to send updated job
details to the GraphQL server.
save() {
this.updateJobGQL.mutate({
input: {
id: this.job.id,
company: this.job.company,
position: this.job.position,
location: this.job.location,
annualBaseSalary: this.job.annualBaseSalary,
description: this.job.description,
responsibilities: this.job.responsibilities,
requirements: this.job.requirements,
applicationInstructions: this.job.applicationInstructions,
status: this.job.status,
tags: this.job.tags
}
}).subscribe(
result => {
this.router.navigateByUrl('/employer/jobs');
}, (error: any) => {
this.dialog.open(DialogComponent, {
width: '250px',
data: { title: 'Oops!', message: error }
});
});
}
On the backend, this triggers the updateJob
mutation executing its resolver function which handles updating the entity in the database. This is defined with the other mutations inside the FullStackJobsMutation class.
Adding a Route Guard
At this point, the application is taking shape with a few view components and routes in which any user could navigate. This state isn't ideal from a security perspective.
Angular makes this problem easy to solve by providing guards we can add to the route configuration to handle this scenario. Using a guard, we can tell the router whether or not it should allow navigation to a requested route based on some condition we specify.
Since we're using JWT access tokens to authorize requests to protected resources on the backend, one way to decide whether or not a route should be accessed is to check the tokenβs expiration time. If it is, the token won't be useful, and the user should be flagged as no longer authenticated.
I added this check by extending the AuthService with a new isAuthenticated()
method that simply checks the validity of the current User
object provided by the oidc-client
library.
...
isAuthenticated(): boolean {
return this.user != null && !this.user.expired;
}
With a method to make a decision about the users's authenticated state, I created the AuthGuard service that will consume it.
The service injects AuthService
and Router
and has a single method called canActivate
, which is necessary to implement Angular's CanActivate
interface.
If the isAuthenticated
method returns true, then navigation can proceed to the requested route; otherwise, the user gets redirected back to the login page.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService: AuthService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.authService.isAuthenticated()) { return true; }
this.router.navigate(['/login'], { queryParams: { redirect: state.url }, replaceUrl: true });
return false;
}
}
To make the Dependency Injection system aware of how to obtain this dependency, we must declare it in the list of providers
belonging to the CoreModule.
This example is a basic route guard that simply tests the authenticated state of the user. The pattern could easily apply to more granular checks, like looking at the user's unique claims and other properties to restrict access to some areas of the application.
Job Management Routing Module
To last big step to tie off the JobManagementModule
is to provide the configuration required to make its routing work.
I added the JobManagementRoutingModule which registers all the child components and their routes with the shell navigation component.
The child routes are protected using the AuthGuard
we just set up as specified by the canActivate
guard property.
const routes: Routes = [
Shell.childRoutes([
{
path: 'employer/job/:id/manage',
component: RootComponent,
canActivate: [AuthGuard],
children: [
{ path: 'basics', component: BasicsComponent },
{ path: 'description', component: DescriptionComponent },
{ path: 'application', component: ApplicationComponent },
{ path: 'tags', component: TagsComponent },
{ path: 'publish', component: PublishComponent }
]
}])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class JobManagementRoutingModule { }
The last thing to do is add the JobManagementModule
to the list of imports in the parent EmployerModule
. With this in place, I fired up the application and was able to log in as an employer and test out the new components.
Jobs Module
At this point, we can create and edit job postings. Now, we're ready to add some view components that will display them in the app.
I added a new jobs module using the Angular CLI to organize the related components for this feature. The first one will be a simple list of published jobs, and the second will display a detailed view of the job posting.
app> ng g module jobs
List Component
The list component is really simple. It calls on the PublicJobsGQL Apollo query service to fetch published job data from the server into the jobs
property which is bound to the template.
export class ListComponent implements OnInit {
jobs: JobSummary[];
busy = false;
constructor(public authService: AuthService, private publicJobsGQL: PublicJobsGQL) { }
ngOnInit() {
this.busy = true;
this.publicJobsGQL.fetch(null, { fetchPolicy: 'network-only' })
.pipe(finalize(() => {
this.busy = false;
})).subscribe(result => {
this.jobs = result.data['publicJobs'];
});
}
}
The PublicJobsGQL
service maps to the publicJobs
query defined by the schema of the GraphQL service.
FieldAsync<ListGraphType<JobSummaryType>>("publicJobs",
resolve: async context =>
{
// Fetch published Jobs from all employers
var jobs = await contextServiceLocator.JobRepository.List(new JobSpecification(j => j.Status == Status.Published));
return jobs.OrderByDescending(j => j.Modified);
}
);
Finally, I made use of the new list by adding an instance of it to the default index component's template.
The app's default view now shows the list of published jobs - π.
Details Component
The details component is setup identically to the list
so no need to cover it separately.
The only difference is that it uses the PublicJobGQL query service to fetch the target job's detail data.
Applicant Module
The final feature we'll add is a convenient way for job seekers to apply to job postings within the app.
I added a new applicant module to organize the code related to job seekers in the same way we use a dedicated employer
module to keep these domains separated in the project.
app> ng g module applicant
Application Button Component
Because we can display job listings in various shapes and locations throughout the app, it would be nice if we had a reusable component we could drop alongside them to let users apply with a single click.
I made it so by adding a simple ApplicationButtonComponent with an apply
method that calls the CreateApplicationGQL mutatation service passing in the target jobId
.
apply() {
this.createApplicationGQL.mutate({
input: {
jobId: this.jobId
}
}).subscribe(
result => {
this.router.navigateByUrl('applicant/confirmation', { state: { data: result.data['createApplication'] } });
}, (error: any) => {
this.dialog.open(DialogComponent, {
width: '250px',
data: { title: 'Oops!', message: error }
});
}
);
}
On the backend, this button triggers the createApplication
mutation operation.
In the resolver function, we extract the jobId
from the input
parameter, fetch the entity from the database and then attach the application by obtaining the user's id from the request context.
Note also the use of AuthorizeWith(Policies.Applicant)
, which ensures only folks possessing the applicant
role claim are authorized to call this operation.
FieldAsync<JobSummaryType>("createApplication", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<CreateApplicationInputType>> { Name = "input" }),
resolve: async context =>
{
var input = context.GetArgument<dynamic>("input");
var job = await contextServiceLocator.JobRepository.GetById(int.Parse(input["jobId"]));
job.AddJobApplication(job.Id, context.GetUserId());
await contextServiceLocator.JobRepository.Update(job);
return job;
}
).AuthorizeWith(Policies.Applicant);
Application Confirmation Component
Finally, I added the ApplicationConfirmationComponent to display a little confirmation message after the user submits an application.
It's so simple, there's nothing significant to point out, but I include it for completeness of the module.
Integrating the Application Button Component
I added the required imports and then dropped an instance of the ApplicationButtonComponent
in the template markup for both the job list and details components.
We control the visibility of the application button so only job seekers see it with ngIf="authService.role==='applicant'"
.
<div fxFlex fxLayoutAlign="end center" class="apply-button-container" *ngIf="authService.role==='applicant'">
<application-button [jobId]="job.id" [applicantCount]="job.applicantCount"></application-button>
</div>
With everything hooked up, when running the app now while logged in as a Job Seeker, the new button is visible and operational.
Wrapping Up
If this guide and demo show anything, it's that there are a lot of moving pieces that must be put in place (correctly) to design and implement the most straightforward applications involving user identity and access control.
There are 3rd party solutions, i.e., Auth0, Azure AD B2C, etc. you can leverage if your scenario supports outsourcing these concerns. If it doesn't, IdentityServer provides an excellent middle ground in handling all the low-level details relating to OAuth2/OpenId Connect while playing nicely with your existing custom identity mechanisms and databases.
GraphQL is maturing, and we'll only see further adoption and support for it in the .NET ecosystem. GraphQL for .NET is an excellent library because it makes it quite easy to use your existing code and databases to build GraphQL APIs in .NET.
Apollo Client for Angular provides a simple, yet powerful library for integrating your app with any GraphQL server. We couldn't explore all of its capabilities i.e., caching, error handling, subscriptions, etc. but we got a decent look in this guide at how to configure and use it in Angular to drive the integration with a GraphQL API.
If you have any feedback or suggestions, I would love to hear it in the comments below, thanks!
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 3
Jan 30, 2020
Read moreGet notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.