developers

Migrating an AngularJS App to Angular - Part 3

Learn how to migrate real-world features of an AngularJS 1 application to a fresh Angular 2+ build (Part 3): routing & API with params, authentication.

Nov 14, 201623 min read

Check out the Real-World Angular Series to learn how to build and deploy a full-featured MEAN stack application

, from ideation to production! Start the series here: Real-World Angular Series - Part 1: MEAN Setup and Angular Architecture .

The Branding Guidelines for Angular state that version 1.x should be referred to as AngularJS, whereas all releases from version 2 and up are named Angular. This migration article will continue to use "Angular 1" to refer to AngularJS (1.x) and "Angular 2" to refer to Angular (2 and up) in order to clearly differentiate the frameworks and reduce confusion.

TL;DR: Many AngularJS 1.x developers are interested in Angular 2, but the major differences between versions 1 and 2 are daunting when we have so many Angular 1 apps already in production or maintenance. In Part 1 and Part 2 of this tutorial we set up our Angular 2 app, migrated the basic architecture, routing, API, and more. In this final installment we'll finish our app! The final code for our Angular 2 app can be cloned from the ng2-dinos GitHub repo.


Recap and Introduction to Part 3

In Migrating an Angular 1 App to Angular 2 - Part 1 we introduced the Angular 1 ng1-dinos Single Page Application and migrated the basic app architecture to Angular 2 ng2-dinos. After Migrating an Angular 1 App to Angular 2 - Part 2 we've migrated pages and routing, getting API data, and filtering.

The third and final part of the tutorial will cover:

  • Routing detail pages
  • Dinosaur detail model and calling the sample-nodeserver-dinos API for dino information by ID
  • Loading states for API calls
  • Aside: user and API authentication with Auth0 and JSON Web Tokens

Note: Remember that we're migrating an Angular 1 app to Angular 2 with a fresh build. We're not upgrading the original Angular 1 codebase.

Setup and Dependencies

Part three has the same dependencies as the first two parts of our tutorial, so make sure you have:

We'll pick up right where we left off.

Migrating Detail Component to Angular 2

Our Angular 1 ng1-dinos app shows a dinosaur's details when we click on one in the homepage listing. We'll implement this in our Angular 2 app now.

Let's create a new detail component:

$ ng g component pages/detail

Routing with Parameters

Let's make our detail component accessible in the application. We want to show the detail page with a dinosaur ID, like this:

http://localhost:4200/dinosaur/5
. Open the
app-routing.module.ts
file:

// ng2-dinos/src/app/core/app-routing.module.ts

...
import { DetailComponent } from '../pages/detail/detail.component';

...
    RouterModule.forRoot([
      ...
      {
        path: 'dinosaur/:id',
        component: DetailComponent
      },
      {
        path: '**',
        component: Error404Component
      }
    ])
...

We'll import our new detail component and then add a route with an

:id
parameter. This route should be placed above the
**
wildcard route.

Linking to Routes with Parameters

Now we need to link each dinosaur with its detail page. Open

dino-card.component.html
:

<!-- ng2-dinos/src/app/pages/home/dino-card/dino-card.component.html -->
...
    <p class="text-center">
      <a class="btn btn-primary" [routerLink]="['/dinosaur', dino.id]">Details</a>
    </p>
...

We'll use the

routerLink
directive with the Details button and bind an array of the URL segments:
[routerLink]="['/dinosaur', dino.id]"
. Now we should be able to click on dinosaur Details in the homepage and see our detail component.

Calling the API for Data by ID

Our detail component needs to make API calls to retrieve dinosaur data by ID. Let's implement this functionality using a new model and a new observable in the

Dinos
service.

Create a Dino Details Model

The Dinos Node API supports a route that accepts an ID and returns detailed dinosaur information. Let's create a model for this. Make sure the local Node API is running and we'll test out the route by accessing it in the browser: http://localhost:3001/api/dinosaur/1. The response looks like this:

// http://localhost:3001/api/dinosaur/1

{
  "id": 1,
  "name": "Allosaurus",
  "pronunciation": "AL-oh-sore-us",
  "meaningOfName": "other lizard",
  "diet": "carnivorous",
  "length": "12m",
  "period": "Late Jurassic",
  "mya": "156-144",
  "info": "Allosaurus was an apex predator in the Late Jurassic in North America."
}

Let's supply a model for this data shape. Create a new file in the

models
directory we created in Part 2 and name it
dino-detail.model.ts
:

// ng2-dinos/src/app/core/models/dino-detail.model.ts

export class DinoDetail {
  constructor(
    public id: number,
    public name: string,
    public pronunciation: string,
    public meaningOfName: string,
    public diet: string,
    public length: string,
    public period: string,
    public mya: string,
    public info: string
  ) { }
}

Add HTTP Observable to Get Dinosaur by ID

Next we'll add the HTTP observable to call the API and retrieve the dinosaur data by ID. Let's open our

dinos.service.ts
file and add a new method:

// ng2-dinos/src/app/core/dinos.service.ts

...
import { DinoDetail } from './models/dino-detail.model';

...
  getDino$(id: number): Observable<DinoDetail> {
    return this.http
      .get(`${this.baseUrl}dinosaur/${id}`)
      .catch(this.handleError);
  }
...

We'll import the

DinoDetail
model we just created. Then we'll create an HTTP observable that accepts an
id: number
as a parameter. The observable has a type annotation of
Observable<DinoDetail>
. The ID parameter is passed to the
GET
request. The handlers we set up in Part 2 are then used for successes and errors. The
catch
operator will generate an observable that terminates with an error.

Using API Data in Detail Component

Now we're ready to get and display individual dinosaur information in our detail component.

Detail Component TypeScript

Let's update the

detail.component.ts
file:

// ng2-dinos/src/app/pages/detail/detail.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Params } from '@angular/router';

import { DinosService } from '../../core/dinos.service';
import { DinoDetail } from '../../core/models/dino-detail.model';

@Component({
  selector: 'app-detail',
  templateUrl: './detail.component.html',
  styleUrls: ['./detail.component.scss']
})
export class DetailComponent implements OnInit {
  dino: DinoDetail;
  error: boolean;

  constructor(
    private titleService: Title,
    private dinosService: DinosService,
    private route: ActivatedRoute) { }

  getDino() {
    this.route.params.forEach((params: Params) => {
      let id = +params['id'];    // convert string to number

      this.dinosService.getDino$(id)
        .subscribe(
          res => {
            this.dino = res;
            this.titleService.setTitle(this.dino.name);
          },
          err => {
            this.error = true;
          }
        );
    });
  }

  ngOnInit() {
    this.getDino();
  }

}

Most of this should look familiar from implementing our home component in Part 2 of the tutorial.

Let's start by importing our dependencies. We need the

Title
service. We'll also need
ActivatedRoute
and
Params
from
@angular/router
in order to retrieve the route ID parameter to use to get the appropriate dinosaur data from the API. Finally, we'll also need the
DinosService
and
DinoDetail
model.

We'll create a couple of properties:

dino
will utilize the
DinoDetail
model type and
error
is a boolean, like in our
home.component.ts
. Then we'll add dependencies to the constructor function so we can use them.

The

getDino()
method iterates over the available route parameters. We'll convert the
id
string to a number and then pass it to the
getDino$(id)
observable. We'll subscribe to the observable and assign the JSON response to the
dino
property. We'll also set the page title as the dinosaur's
name
. If there's an error retrieving data, we'll simply set the
error
property to
true
.

Finally, we'll call the

getDino()
method in the
ngOnInit()
lifecycle hook.

Detail Component Template

Now we're ready to display the dinosaur detail information in our detail component template. Open the

detail.component.html
file:

<!-- ng2-dinos/src/app/pages/detail/detail.component.html -->
<article id="content-wrapper" class="content-wrapper">
  <section *ngIf="dino" id="detail-content-dinosaur" class="panel panel-default">
    <div class="panel-heading">
      <h2 class="text-center">{{dino.name}}</h2>
    </div>
    <ul class="list-group">
      <li class="list-group-item">
        <h4 class="list-group-item-heading">Pronunciation:</h4>
        <p class="list-group-item-text">
          <em>{{dino.pronunciation}}</em>
        </p>
      </li>
      <li class="list-group-item">
        <h4 class="list-group-item-heading">Name Means:</h4>
        <p class="list-group-item-text">{{dino.meaningOfName}}</p>
      </li>
      <li class="list-group-item">
        <h4 class="list-group-item-heading">Length:</h4>
        <p class="list-group-item-text">{{dino.length}}</p>
      </li>
      <li class="list-group-item">
        <h4 class="list-group-item-heading">Diet:</h4>
        <p class="list-group-item-text">{{dino.diet}}</p>
      </li>
      <li class="list-group-item">
        <h4 class="list-group-item-heading">Lived:</h4>
        <p class="list-group-item-text">
          {{dino.period}}<br>
          <em>({{dino.mya}} million years ago)</em>
        </p>
      </li>
    </ul>
    <div class="panel-body">
      <p class="lead" [innerHTML]="dino.info"></p>
    </div>
    <div class="panel-footer">
      <a routerLink="/">&larr; All Dinosaurs</a>
    </div>
  </section>
  <!-- Error -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Rawr!</strong> There was an error retrieving data for the dinosaur you requested.
  </p>
</article>

Like with the other page components we migrated, we don't need a

.detail-wrapper
class in the template. In Angular 1 ng1-dinos we used these classes to "componetize" globally-declared CSS. Angular 2 encapsulates styles by component so we don't need specific wrapper classes anymore.

We'll use Bootstrap to style most of our dinosaur details. Most of our data can be displayed simply using interpolation with double-curly braces. The exception is the

info
paragraph. Our API sometimes returns HTML markup in this string. In Angular 1 ng-dinos, we used
ng-bind-html
to render markup in bindings. In Angular 2, we need to bind to the
innerHTML
DOM property like so:

<p class="lead" [innerHTML]="dino.info"></p>

We'll add a link back to the homepage and then finally, show an error message if there was a problem retrieving data from the API.

Detail Component Styles

We'll just make one small tweak in the SCSS for our detail component to reduce the amount of extra space above the dinosaur name heading. In the Angular 1 app, the detail page styles were here:

ng1-dinos/src/assets/css/scss/pages/_detail.scss
.

Our Angular 2 ng2-dinos detail component styles should look like this:

/* ng2-dinos/src/app/pages/detail/detail.component.scss */

/*--------------------
       DETAIL
--------------------*/

.panel-heading h2 {
  margin-top: 10px;
}

Now we have our detail component! When dinosaur details are clicked on the homepage, the detail pages should look something like this:

Angular 1 migrate to Angular 2 detail route

Browse your app to make sure this is working as expected.

Loading State for API Calls

Our Angular 1 to Angular 2 migration is almost complete! The last piece is a simple loading state that needs to be shown while API calls are resolving. Because we're running our app and API locally, communication between the two is almost instantaneous. In another environment this may not be the case. We'll implement a small loading state to show while data is being retrieved. This will show in the home and detail components.

In Angular 1 ng1-dinos, this loading state was a simple directive at

ng1-dinos/src/app/core/ui/loading.dir.js
. In Angular 2, we'll create a very similar loading component.

Loading Image Asset

The first thing we need is the image asset for the loading state. This can be downloaded from the Angular 1 ng1-dinos app here:

ng1-dinos/src/assets/images/raptor-loading.gif
. We'll place this image in our Angular 2 ng2-dinos app in an equivalent location:
ng2-dinos/src/assets/images/
.

Loading Component TypeScript

The loading component will be one flat file, so we'll add some flags to the CLI to generate it:

ng g component core/ui/loading --it --is --flat

The

--it
flag is shorthand for
inline-template
. The
--is
is shorthand for
inline-styles
, and
--flat
indicates a containing folder should not be generated.

Note: You can also add

--no-spec
when generating CLI files if you don't want test files.

Open the new

loading.component.ts
:

// ng2-dinos/src/app/core/ui/loading.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-loading',
  template: '<img class="loading" src="/assets/images/raptor-loading.gif"/>',
  styles: [`
    .loading {
      display: block;
      margin: 30px auto; }
  `]
})
export class LoadingComponent { }

It's possible to keep everything we need in the component without external template or style files. Instead of using

templateUrl
we'll use
template
. The template consists of an image tag with our
raptor-loading.gif
file. Instead of
styleUrls
, we can use a
styles
array and add CSS rulesets right in the component. We'll use an ES6 template string literal (in backticks) to maintain readability.

Add Loading Component to App Module

In order to use our new component in our app, we need to add it to our

app.module.ts
:

// ng2-dinos/src/app/app.module.ts

...
import { LoadingComponent } from './core/ui/loading.component';
...

@NgModule({
  declarations: [
    ...
    LoadingComponent,
    ...
  ],
  ...
})
export class AppModule { }

We'll import the

LoadingComponent
class and then add it to the
declarations
array. Now we can use the
<app-loading>
element in other components.

Add Loading Component to Home Component

The Angular 1 ng1-dinos app shows the loading directive in the home and detail views.

Implement Loading Functionality in Home Component TypeScript

In

home.component.ts
, let's add the functionality we need to conditionally add our new loading component:

// ng2-dinos/src/app/pages/home/home.component.ts

...

export class HomeComponent implements OnInit {
  ...
  loading: boolean;

  constructor(...) { }

  getDinos() {
    this.dinosService.getAllDinos$()
      .subscribe(
        res => {
          ...
          this.loading = false;
        },
        err => {
          ...
          this.loading = false;
        }
      );
  }

  ngOnInit() {
    ...
    this.loading = true;
    this.getDinos();
  }

  ...

  get isLoaded() {
    return this.loading === false;
  }

}

We'll add a boolean

loading
property to track loading state. Loading should be turned off when the API responds either with a success or a failure; we don't want to get stuck in an infinite loading state. We'll add
this.loading = false
in both the
onNext
and
onError
subscription functions.

Note: This differs from our implemention in the Angular 1 app: ng1-dinos used the promise method

.finally()
. When subscribing to observables, the
onCompleted
function is only executed upon graceful termination of the observable sequence. Unlike
finally()
with promises, it will not run if an exception occurs.

To initiate the loading state, we'll set the

loading
property to
true
in the
ngOnInit()
lifecycle hook.

Finally, we need a getter method

get isLoaded()
to tell the template when loading has completed. Angular 1 ng1-dinos implemented this expression in the template, but the Angular 2 docs recommend moving this kind of logic to the component.

Implement Loading Functionality in Home Component Template

Now we need to implement our loading component and some template logic in the home markup

home.component.html
:

<!-- ng2-dinos/src/app/pages/home/home.component.html -->
<article id="content-wrapper" class="content-wrapper">
  <h2 class="content-heading">{{pageName}}</h2>
  <app-loading *ngIf="loading"></app-loading>
  <div *ngIf="isLoaded">
    <!-- Search dinosaurs -->
    ...
    <!-- Dinosaurs -->
    ...
    <!-- No search results -->
    ...
    <!-- Error -->
    ...
  </div>
</article>

We'll add and remove the loading component with

<app-loading *ngIf="loading">
. We'll also add a container around the rest of the page content and only stamp it if the
isLoaded
getter is true.

When our app home component is loading, it now looks like this:

Angular 2 app migration loading state

The animated gif shows a running raptor until loading is completed.

Add Loading Component to Detail Component

Now we'll make similar changes to the detail component to add the loading state.

Implement Loading Functionality in Detail Component TypeScript

Let's open our

detail.component.ts
file:

// ng2-dinos/src/app/pages/detail/detail.component.ts

...

export class DetailComponent implements OnInit {
  ...
  loading: boolean;

  constructor(...) { }

  getDino() {
    ...
      this.dinosService.getDino$(id)
        .subscribe(
          res => {
            ...
            this.loading = false;
          },
          err => {
            ...
            this.loading = false;
          }
        );
    ...
  }

  ngOnInit() {
    this.loading = true;
    this.getDino();
  }

  get isLoaded() {
    return this.loading === false;
  }

}

We'll make the same changes to our detail component as the home component. We want to add a boolean

loading
property that is
true
on initialization and
false
onNext
and
onError
. A
get isLoaded()
getter compares the loading state to check if it's been set to
false
and will be used to stamp content in the template.

Implement Loading Functionality in Detail Component Template

Open

detail.component.html
:

<!-- ng2-dinos/src/app/pages/detail/detail.component.html -->
<article id="content-wrapper" class="content-wrapper">
  <app-loading *ngIf="loading"></app-loading>
  <div *ngIf="isLoaded">
    <!-- Dinosaur details -->
    ...
    <!-- Error -->
    ...
  </div>
</article>

Let's add the

<app-loading>
element and a wrapper to hide the content while loading is in progress. Now the loading gif should show while we retrieve API data for a dinosaur's detail information.

Remove "Loading..." Text from Index HTML

Finally, we're going to remove the

Loading...
text from our
index.html
file's
<app-root>
element. This is the last thing we'll do to make our Angular 2 migration feature-match our Angular 1 ng1-dinos app:

<!-- ng2-dinos/src/index.html -->
...
<body>
  <app-root></app-root>
</body>
...

Completed Migration From Angular 1 to Angular 2

The migration of our Angular 1 ng1-dinos app to Angular 2 ng2-dinos is now complete! If you have both apps running, they should be functionally equivalent from a user's perspective. Please explore the two apps in the browser to make sure that our migration was successful.

Aside: Refactoring Suggestions

Here are my refactoring suggestions from part three of our migration tutorial:

  • As with Part 2, you may want to consider using additional
    @NgModule
    s to manage dependencies. Modules can make dependency management easier. Read the Angular Modules docs and Use @NgModule to Manage Dependencies in your Angular 2 Apps to learn more.
  • You could potentially abstract the template API error markup into its own component. The error message is currently different between the home and detail page components, but you could use data binding to pass a custom string into the component each time it's utilized. This might help with scalability if additional API calls will be made in new components in the future.

Aside: Authenticate an Angular App and Node API with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with an Angular application and a Node API using Auth0. You can clone this sample app and API from the angular-auth0-aside repo on GitHub.

Auth0 hosted login screen

Features

The sample Angular application and API has the following features:

  • Angular application generated with Angular CLI and served at http://localhost:4200
  • Authentication with auth0.js using a hosted Lock instance
  • Node server protected API route
    http://localhost:3001/api/dragons
    returns JSON data for authenticated
    GET
    requests
  • Angular app fetches data from API once user is authenticated with Auth0
  • Profile page requires authentication for access using route guards
  • Authentication service uses a subject to propagate authentication status events to the entire app
  • User profile is fetched on authentication and stored in authentication service
  • Access token, ID token, profile, and token expiration are stored in local storage and removed upon logout

Sign Up for Auth0

You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 client app and API so Auth0 can interface with an Angular app and Node API.

Set Up a Client App

  1. Go to your Auth0 Dashboard and click the "create a new client" button.
  2. Name your new app and select "Single Page Web Applications".
  3. In the Settings for your new Auth0 client app, add
    http://localhost:4200/callback
    to the Allowed Callback URLs and
    http://localhost:4200
    to the Allowed Origins (CORS).
  4. Scroll down to the bottom of the Settings section and click "Show Advanced Settings". Choose the OAuth tab and set the JsonWebToken Signature Algorithm to
    RS256
    .
  5. If you'd like, you can set up some social connections. You can then enable them for your app in the Client options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure you set up your own social keys and do not leave social connections set to use Auth0 dev keys.

Set Up an API

  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to your API endpoint URL. In this example, this is
    http://localhost:3001/api/
    . The Signing Algorithm should be
    RS256
    .
  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our Angular client and Node backend API.

Dependencies and Setup

The Angular app utilizes the Angular CLI. Make sure you have the CLI installed globally:

$ npm install -g @angular/cli

Once you've cloned the project, install the Node dependencies for both the Angular app and the Node server by running the following commands in the root of your project folder:

$ npm install
$ cd server
$ npm install

The Node API is located in the

/server
folder at the root of our sample application.

Open the

server.js
file:

// server/server.js
...
// @TODO: change [CLIENT_DOMAIN] to your Auth0 domain name.
// @TODO: change [AUTH0_API_AUDIENCE] to your Auth0 API audience.
var CLIENT_DOMAIN = '[CLIENT_DOMAIN]'; // e.g., youraccount.auth0.com
var AUTH0_AUDIENCE = '[AUTH0_API_AUDIENCE]'; // http://localhost:3001/api in this example

var jwtCheck = jwt({
    secret: jwks.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${CLIENT_DOMAIN}/.well-known/jwks.json`
    }),
    aud: AUTH0_AUDIENCE,
    issuer: `https://${CLIENT_DOMAIN}/`,
    algorithm: 'RS256'
});
...
//--- GET protected dragons route
app.get('/api/dragons', jwtCheck, function (req, res) {
  res.json(dragonsJson);
});
...

Change the

CLIENT_DOMAIN
variable to your Auth0 client domain. The
/api/dragons
route will be protected with express-jwt and jwks-rsa.

Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.

Our API is now protected, so let's make sure that our Angular application can also interface with Auth0. To do this, we'll activate the

src/app/auth/auth0-variables.ts.example
file by deleting the
.example
from the file extension. Then open the file and change the
[CLIENT_ID]
and
[CLIENT_DOMAIN]
strings to your Auth0 information:

// src/app/auth/auth0-variables.ts
...
export const AUTH_CONFIG: AuthConfig = {
  CLIENT_ID: '[CLIENT_ID]',
  CLIENT_DOMAIN: '[CLIENT_DOMAIN]',
  ...

Our app and API are now set up. They can be served by running

ng serve
from the root folder and
node server.js
from the
/server
folder.

With the Node API and Angular app running, let's take a look at how authentication is implemented.

Authentication Service

Authentication logic on the front end is handled with an

AuthService
authentication service:
src/app/auth/auth.service.ts
file
.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import * as auth0 from 'auth0-js';
import { AUTH_CONFIG } from './auth0-variables';
import { UserProfile } from './profile.model';

@Injectable()
export class AuthService {
  // Create Auth0 web auth instance
  // @TODO: Update AUTH_CONFIG and remove .example extension in src/app/auth/auth0-variables.ts.example
  auth0 = new auth0.WebAuth({
    clientID: AUTH_CONFIG.CLIENT_ID,
    domain: AUTH_CONFIG.CLIENT_DOMAIN,
    responseType: 'token id_token',
    redirectUri: AUTH_CONFIG.REDIRECT,
    audience: AUTH_CONFIG.AUDIENCE,
    scope: AUTH_CONFIG.SCOPE
  });
  userProfile: UserProfile;

  // Create a stream of logged in status to communicate throughout app
  loggedIn: boolean;
  loggedIn$ = new BehaviorSubject<boolean>(this.loggedIn);

  constructor(private router: Router) {
    // If authenticated, set local profile property and update login status subject
    if (this.authenticated) {
      this.userProfile = JSON.parse(localStorage.getItem('profile'));
      this.setLoggedIn(true);
    }
  }

  setLoggedIn(value: boolean) {
    // Update login status subject
    this.loggedIn$.next(value);
    this.loggedIn = value;
  }

  login() {
    // Auth0 authorize request
    this.auth0.authorize();
  }

  handleAuth() {
    // When Auth0 hash parsed, get profile
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        window.location.hash = '';
        this._getProfile(authResult);
        this.router.navigate(['/']);
      } else if (err) {
        this.router.navigate(['/']);
        console.error(`Error: ${err.error}`);
      }
    });
  }

  private _getProfile(authResult) {
    // Use access token to retrieve user's profile and set session
    this.auth0.client.userInfo(authResult.accessToken, (err, profile) => {
      this._setSession(authResult, profile);
    });
  }

  private _setSession(authResult, profile) {
    // Save session data and update login status subject
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('profile', JSON.stringify(profile));
    localStorage.setItem('expires_at', authResult.expiresAt);
    this.userProfile = profile;
    this.setLoggedIn(true);
  }

  logout() {
    // Remove tokens and profile and update login status subject
    localStorage.removeItem('access_token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('profile');
    localStorage.removeItem('expires_at');
    this.userProfile = undefined;
    this.setLoggedIn(false);
  }

  get authenticated(): boolean {
    // Check if current time is past access token's expiration
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return Date.now() < expiresAt;
  }

}

This service uses the config variables from

auth0-variables.ts
to instantiate an
auth0.js
WebAuth instance.

An RxJS

BehaviorSubject
is used to provide a stream of authentication status events that you can subscribe to anywhere in the app.

The

login()
method authorizes the authentication request with Auth0 using your config variables. An Auth0 hosted Lock instance will be shown to the user and they can then log in.

Note: If it's the user's first visit to our app and our callback is on

localhost
, they'll also be presented with a consent screen where they can grant access to our API. A first party client on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive an

id_token
,
access_token
, and
expires_at
in the hash from Auth0 when returning to our app. The
handleAuth()
method uses Auth0's
parseHash()
method callback to get the user's profile (
_getProfile()
) and set the session (
_setSession()
) by saving the tokens, profile, and token expiration to local storage and updating the
loggedIn$
subject so that any subscribed components in the app are informed that the user is now authenticated.

Note: The profile takes the shape of

profile.model.ts
from the OpenID standard claims.

The

handleAuth()
method can then be called in the
app.component.ts
constructor
like so:

// src/app/app.component.ts
import { AuthService } from './auth/auth.service';
...
  constructor(private auth: AuthService) {
    // Check for authentication and handle if hash present
    auth.handleAuth();
  }
...

Finally, we have a

logout()
method that clears data from local storage and updates the
loggedIn$
subject. We also have an
authenticated
accessor to return current authentication status.

Once

AuthService
is provided in
app.module.ts
, its methods and properties can be used anywhere in our app, such as the home component.

The callback component is where the app is redirected after authentication. This component simply shows a loading message until hash parsing is completed and the Angular app redirects back to the home page.

Making Authenticated API Requests

In order to make authenticated HTTP requests, we need to add a

Authorization
header with the access token in our
api.service.ts
file
.

// src/app/api.service.ts
...
  getDragons$(): Observable<any[]> {
    return this.http
      .get(`${this.baseUrl}dragons`, {
        headers: new HttpHeaders().set(
          'Authorization', `Bearer ${localStorage.getItem('access_token')}`
        )
      })
      .catch(this._handleError);
  }
...

Final Touches: Route Guard and Profile Page

A profile page component can show an authenticated user's profile information. However, we only want this component to be accessible if the user is logged in.

With an authenticated API request and login/logout implemented, the final touch is to protect our profile route from unauthorized access. The

auth.guard.ts
route guard can check authentication and activate routes conditionally. The guard is implemented on specific routes of our choosing in the
app-routing.module.ts
file
like so:

// src/app/app-routing.module.ts
...
import { AuthGuard } from './auth/auth.guard';
...
      {
        path: 'profile',
        component: ProfileComponent,
        canActivate: [
          AuthGuard
        ]
      },
...

More Resources

That's it! We have an authenticated Node API and Angular application with login, logout, profile information, and protected routes. To learn more, check out the following resources:

Conclusion

Our ng2-dinos app is complete! Make sure you've run

ng lint
and corrected any issues. With clean code, we shouldn't have any errors. We've successfully migrated the dinosaur detail pages and implemented a simple loading state.

Hopefully you're now ready to dive into Angular migrations as well as new Angular 2 projects with confidence!