Bootstrap

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's Authorization header. The token is fetched from the authorizationHeaderValue property we added as part of the AuthService 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!

Source code here

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
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 1

Dec 8, 2019

Read more
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 2
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 2

Dec 30, 2019

Read more
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 3
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 3

Jan 30, 2020

Read more
Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.