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

Welcome to part two 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 kicked things off on the backend by making an IdentityServer powered ASP.NET Core application that will provide our Angular SPA with some basic but essential capabilities to create and authenticate our application's users.

If you'd like to get fully up to speed before proceeding with this portion of the guide, please go back and check out the first post.

In this one, we'll be jumping into the frontend. We'll begin scaffolding out our Angular application and then add some basic UI to support user signup and login while integrating these features with the existing backend bits we created previously. Sound good? πŸ‘

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 πŸ‘ˆ You're here!
  • Part 3: Implementing an ASP.NET Core GraphQL API with authorization using GraphQL .NET

Development Environment

As of December 2019, 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
Get notified on new posts

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

Creating the Angular App

Using the Angular CLI, I spun up a new app skeleton using the ng new command.

Spa> ng new fullstack-jobs --routing=true --style=scss --directory ./

Next, I started up the project using the ng serve command to build and run the app on the CLI development server at http://localhost:4200.

With the app running, I pointed my browser to localhost:4200, and the app loaded successfully. βœ”οΈ

Adding Angular Material

Angular Material provides a comprehensive set of polished UI components developed by the Angular team that we can leverage to get our project off the ground quickly. To get started, I followed the steps in Angular's getting started guide.

I installed Angular Material by running the ng add command from the root of the project folder.

Spa> ng add @angular/material

The ng add command uses the CLI's schematics tool to setup Angular Material in the project by making the sure the right dependencies are added to package.json and performing some other important setup and initialization tasks like enabling animations, gesture support and providing some basic styling.

Custom Angular Material Theme

During the setup process, there's an option to use a pre-built theme or a custom one. I chose the custom route and made a few changes to add some specific colors, styles, and font. Theming in Angular Material is reasonably straightforward. The most critical step is defining three color palettes for the main theme colors β€” primary, accent, and warn.

Following the theming guide I added a new theme.scss sass file and defined a variable for each required palette based on the Material Design palettes available within Angular Material.

Next, I created the $fullstack-jobs-theme object using the mat-light-theme function with the palette variables. Angular Material also provides also a mat-dark-theme function which produces dark backgrounds and light elements. You can also check out more available color palettes here: https://material.io/design/color/.

@import '~@angular/material/theming';

$fullstack-jobs-theme-primary: mat-palette($mat-light-blue);
$fullstack-jobs-theme-accent: mat-palette($mat-orange, A200, A100, A400);
$fullstack-jobs-theme-warn: mat-palette($mat-red);

$fullstack-jobs-theme: mat-light-theme(
    $fullstack-jobs-theme-primary,
    $fullstack-jobs-theme-accent,
    $fullstack-jobs-theme-warn
);

Custom Typography

Similar to creating a custom theme, we can leverage Angular Material's Sass-based theming again to create a custom typography configuration.

Using the mat-typography-config function we can specify the font-family and default sizes for a couple of typography levels. Here's a complete list of Angular Material's typography levels which can be included in the typography config.

...
$custom-typography: mat-typography-config(
  $font-family: 'Nunito, sans-serif',
  $headline: mat-typography-level(32px, 48px, 700),
  $body-1: mat-typography-level(16px, 24px, 500)
);

Because we're using an external font (Nunito), we must also modify the index.html file by adding a link to download the font from google.

<link href="https://fonts.googleapis.com/css?family=Nunito:300,400,600&display=swap" rel="stylesheet">

With our custom theme defined, we just need to tie it into the main style file of our application by adding the required imports to styles.scss.

...
@import "theme.scss";
@import "variables.scss";
@include angular-material-theme($fullstack-jobs-theme);
@include angular-material-typography($custom-typography);
...

Import the Angular Material Component Modules

Angular Material provides modules for each of its components so our app can import only the ones that we need, which helps keeps the bundle size under control.

To prevent cluttering up app.module with the Angular Material imports I created a wrapper module that imports and exports the list of components we'll use in the app.

Getting Modular

Modules allow us to easily organize specific parts of our application domain or infrastructure by grouping related components, services, directives, etc. together both logically in code and physically in the application's folder structure.

They also allow us to export and selectively share functionality with the rest of the application. We did this in the previous step with the module we created to import/export all of the Angular Material components our app requires.

To get some basic building blocks in place, I created three more primary modules using the ng g module <name> CLI command.

  • shared - We'll keep any code i.e. components, directives, pipes etc. that are shared globally across different modules in here.

  • shell - Contains the skeleton or root template for the UI. Inside of the shell module I added a header component to display the main navigation header containing the branding logo image, nagivgation links etc.

  • home - Contains landing page component and related bits.

At this point, with the foundational modules in place, the folder structure is starting to take shape.

src/
|- app/               app components
|  |- home/           landing page etc.
|  |- material/       angular material wrapper module
|  |- shared/         shared module  (common components, directives etc.)
|  |- shell/          root ui template
...

To make the new modules available to the application the final step is importing them into the main app.module. Note, the MaterialModule is imported by SharedModule so it does not require another import here.

...
/* Module Imports */
import { HomeModule } from './home/home.module';
import { ShellModule } from './shell/shell.module';
import { SharedModule } from './shared/shared.module';

Hello World

With everything in place, I built and ran the project on the node development server using the ng serve CLI command, refreshed my browser at localhost:4200 and saw the updated changes. πŸ‘€

Pretty barebones at this point, but all of our foundational pieces are working. The header is displayed and styled using the custom theme we set up along with Angular Material's mat-toolbar component.

Finally, the app automatically routed to the default index component inside of the HomeModule as defined in home-routing-module.

User Signup and Authentication

We're ready to extend the app with some functionality to support the user signup and login workflows. These features will rely on the ASP.NET Core API and IdentityServer work we did in the previous post.

Services

We need to implement a few new Angular services that our UI components can use to talk to the backend.

Account Service

We'll start with the AccountService and add a simple signup() method which uses Angular's HttpClient to make a request to the AccountsController API we created in the previous post.

Note, before you can use the HttpClient, you must import the Angular HttpClientModule into AppModule.

export class AccountService extends BaseService  {

  constructor(private http: HttpClient, private configService: ConfigService) { 
    super();   
  }

  signup(accountSignup: AccountSignup) {    
    return this.http.post(this.configService.authApiURI + '/accounts', accountSignup).pipe(catchError(this.handleError));
  }
}

We're taking advantage of TypeScript's object-oriented support for inheritance here by deriving from BaseService which contains some shared logic we can use in all of our http services.

Another shared class we'll use to support some of our services is the ConfigService which provides a central location for storing things like the URLs of the backend APIs.

Auth Service

Now that we have a service in place for creating new users let’s create one that can handle authentication and managing user sessions.

To handle all interactions with our OpenID Connect Provider, we'll be using the oidc-client library. So, first step is to pull this in as a dependency in our package.json file.

Spa> npm install oidc-client --save

Next, using the CLI, I generated a new service in Core/Authentication.

Authentication> ng g service auth

The primary feature of the oidc-client library is the UserManager class, which provides a simple, high-level API for signing a user in, signing out and managing the user's claims and access tokens returned from the OIDC/OAuth2 provider.

To use it, we need to import a few types from the package.

import { UserManager, User } from 'oidc-client';

The UserManager constructor requires a settings object as a parameter. There are a few required settings we must have in place for the OIDC client to work properly.

  • authority: The URL of the OIDC/OAuth2 provider.
  • client_id: Your client application's identifier as registered with the OIDC/OAuth2 provider.
  • redirect_uri: The redirect URI of your client application to receive a response from the OIDC/OAuth2 provider.
  • response_type: The type of response desired from the OIDC/OAuth2 provider.
  • scope: The scope being requested from the OIDC/OAuth2 provider.

I plugged in the required configuration values for the client. Note, the authority is being sourced from the configService. For demo purposes, we're hardcoding them, but ideally, in production, they'll be managed as part of your existing approach to environment configuration.

private manager = new UserManager({
      authority: this.configService.authAppURI,
      client_id: 'angular_spa',
      redirect_uri: 'http://localhost:4200/auth-callback',
      post_logout_redirect_uri: 'http://localhost:4200/',
      response_type: "code",
      scope: "openid profile email api.read",
      filterProtocolClaims: true,
      loadUserInfo: true
});

With the UserManager initialized, we're able to use it in our new login() method.

This method is rather simple with all of the heavy lifting performed by the oidc-client signinRedirect() call, which automatically redirects users to our OpenID Connect provider using requests configured based on the UserManagerSettings we specified in the previous step.

The extraQueryParams represent an optional parameter object we pass along to the backend during the new user signup and login scenario.

login(newAccount?: boolean, userName?: string) {

   let extraQueryParams = newAccount && userName ? {
       newAccount: newAccount,
       userName: userName
   } : {};

   return this.manager.signinRedirect({
      extraQueryParams
   });
}

To complete the authentication flow, we must call the oidc-client's signinRedirectCallback() method to process the response we get back from the OpenID Connect Provider. This redirect is the location of the redirect_uri property we specified in our client configuration.

I added a new completeAuthentication() method to do just that as well as a BehaviorSubject from the RxJS library in authNavStatusSource to store and emit the current user's authentication status obtained from our new isAuthenticated(). We'll use this observable value in the next step to help display the proper state in the navigation bar depending on whether the user is logged-in or not.

async completeAuthentication() {
   this.user = await this.manager.signinRedirectCallback();
   this.authNavStatusSource.next(this.isAuthenticated());
}

isAuthenticated(): boolean {
   return this.user != null && !this.user.expired;
}

Register the Angular Client with IdentityServer

In the last post, we created a client configuration to use our test JavaScript application with IdentityServer. Now, we need to define a real configuration to enable proper integration between our Angular application and IdentityServer.

In the AuthServer project I extended the client definitions in Config by adding a new entry for our app.

...
new Client {
 RequireConsent = false,
 ClientId = "angular_spa",
 ClientName = "Angular Client",
 AllowedGrantTypes = GrantTypes.Code,
 RequirePkce = true,
 RequireClientSecret = false,
 AllowedScopes = { "openid", "profile", "email", "api.read" },
 RedirectUris = {"http://localhost:4200/auth-callback"}, // test client runs on same host
 AllowedCorsOrigins = {"http://localhost:4200" }, // test client runs on same host
 AccessTokenLifetime = (int)TimeSpan.FromMinutes(120).TotalSeconds
}

Header Component

Back on the client app, I added a header component earlier to represent the patch of screen in our app that will display the top navigation bar and logo image in our main layout.

We can extend it now by subscribing to the subject we created in the last step and changing what gets displayed in the header component based on the authentication status.

Here, we're simply setting up a subscription on the value emitted by authNavStatus$ and setting the local isAuthenticated property in the component.

ngOnInit() {
   this.subscription = this.authService.authNavStatus$.subscribe(status => this.isAuthenticated = status);
   this.name = this.authService.name;    
}

In the template, we're just toggling the visibility of the links and showing the username when someone is logged in based on the status of isAuthenticated.

<mat-toolbar color="primary">
    <a mat-button routerLink="/"><img src="assets/images/logo.png" height="28"></a>
    <span class="spacer"></span>  
    <a mat-button routerLink="/signup" *ngIf="!isAuthenticated">Sign up</a>
    <a mat-button routerLink="/login" *ngIf="!isAuthenticated">Log in</a>   
    <a mat-button routerLink="/employer/jobs" *ngIf="isAuthenticated && authService.role=='employer'">My Job Postings</a>  
    <a mat-button (click)="signout()" *ngIf="isAuthenticated">Signout {{name}}</a>       
</mat-toolbar>

Finally, as part of this step, I also introduced a new CoreModule which is where we'll keep these services and some of the other single-use components.

Account Module

The responsibilities of creating and authenticating users don't feel like a natural fit within any of the modules we've created so far. So, using the CLI workflow I set up a new AccountModule and Signup component where we'll start work on the registration form.

Angular Flex-Layout

Before we begin the signup view, there's one design prerequisite we need to get in place. So far, we've included Angular Material and written a little bit of custom CSS. However, we still have no grid or layout model set up in our app styling to help enforce consistent, responsive, and overall decent looking layouts across our UI components.

Enter Angular Flex-Layout which is a package that makes it easy to create flexbox-based page layouts with a set of directives we can use in our component's UI templates.

I followed the instructions to install the package using npm.

Spa> npm i -s @angular/flex-layout @angular/cdk

Finally, to make it available throughout the application I imported and re-exported the FlexLayoutModule within SharedModule.

Signup Component

We now have all of the supporting bits in place to complete the user signup component. I added the necessary markup in the html template and logic in the associated class to setup a form to collect the user data and then call on the AccountService to submit it to the backend API. If everything succeeds, we use call the login() method we just setup on the AuthService to begin the login flow with the OpenID Connect Provider.

At this point, I'm able to navigate to the new signup view, create an account, and trigger the login redirect to the IdentityServer. πŸ’₯

User signup component.

We've written most of the code required to initiate and complete the login flow as part of this step. There's just a couple of more pieces needed to complete the interactive login flow between our Angular app and IdentityServer.

Login Component

In the last post, on the server side we set up an ASP.NET Core MVC login view to provide the UI and controller support for authenticating users against our Identity membership database.

This authentication step also signs the user into IdentityServer. It creates the cookies necessary for our client application to obtain tokens from the token endpoint using the authorization code flow with PKCE OIDC protocol.

In the previous step, we added a new login() method to Angular client's AccountService to initiate the login flow and redirect the user to the ASP.NET Core login view. At the moment, we have no standalone method to initiate this so I created a new login component in the AccountModule to fill this gap.

The component code is very simple, we just call login() on our AccountService during initialization which in turn redirects us to the login page on the server. The template code is even simpler, we just flash up an Angular Material spinner component during the quick transition of the browser redirect out of our Angular app and into the ASP.NET Core site.

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../core/authentication/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  constructor(private authService: AuthService) { }

  async ngOnInit() {
      await this.authService.login();
  }
}

Using the link, we added previously on the header navigation bar, we can navigate to the new login component and trigger a redirect to the AuthServer to begin the interactive login phase of the authentication flow.

Login component.

Callback Component

One of the parameters we saw in the client configuration is redirect_uri, which is a callback location on the client where IdentityServer sends back its response containing any tokens, errors, etc. that result from the authentication attempt.

We've already added some code to process this response via the completeAuthentication() method we added to AuthService.

This code ultimately relies on the signinRedirectCallback() method of the oidc-client to do all of the heavy lifting like the remaining gymnastics to complete the authorization code flow with PKCE by obtaining the authorization code and exchanging it for an access token. Also, because we set loadUserInfo to true in our client configuration, it will call the user info endpoint to get any extra identity data it is authorized to access.

I added a new auth-callback component to the root of the application which simply calls completeAuthentication() on the the AuthService to process the authorization response as mentioned and then route the user to the home page for now. We'll change this bit in the next post to route to a default view based on the type of user i.e. Employer or Job Seeker.

export class AuthCallbackComponent implements OnInit {

  constructor(private authService: AuthService, private router: Router) { }

  async ngOnInit() {
    await this.authService.completeAuthentication();
    this.router.navigateByUrl("/home");
  }
}

Finally, I added a routing entry to the AppRoutingModule to create a path for the route of callback component that will match the value we've specified for the redirect_uri client configuration value.

const routes: Routes = [
  { path: 'auth-callback', component: AuthCallbackComponent },
   // Fallback when no prior route is matched
  { path: '**', redirectTo: '', pathMatch: 'full' }
];

Putting It All Together

With everything in place, we can now complete an end to end flow of the user signup and authentication process. Note, I've also implemented logout by adding the required methods to the AuthService on the Angular side and AccountsController in the AuthServer project.

Angular Aspnet Core User Signup and Oidc Authentication Flow Using Authorization Code With Proof Key for Code Exchange.

Wrapping Up

In this post, we walked through the process to structure and begin building out our Angular client application. It doesn't do much yet, but we've added some key foundational bits like Angular Material and Angular Flex-Layout to improve our component design and spruce up the overall look and feel of the UI.

We also implemented user signup and login features and used the oidc-client library within a custom service to integrate the app with the backend ASP.NET Core API and OpenID Connect Provider.

In the next post, we'll begin to set up our ASP.NET Core-based GraphQL API and see to consume it using Angular Apollo to add some more exciting and powerful features to our app, so stay tuned!

Thanks for reading and happy programming!

Source code here

Get notified on new posts

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

Get notified on new posts
X

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