Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 7b: Relational Data and Token Renewal

DZone's Guide to

Real-World Angular Series, Part 7b: Relational Data and Token Renewal

In this article, we wrap up some of the logic for our application and learn to add authentication using Auth0 and web tokens.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

Welcome back! If you missed Part 1 of this article, check it out here

Angular: Add User's Events Endpoint to API Service

Let's add our new API endpoint to our API service. Open the api.service.tsfile and add this method:

// src/app/core/api.service.ts
...
  // GET all events a specific user has RSVPed to (login required)
  getUserEvents$(userId: string): Observable<EventModel[]> {
    return this.http
      .get(`${ENV.BASE_API}events/${userId}`, {
        headers: new HttpHeaders().set('Authorization', this._authHeader)
      })
      .catch(this._handleError);
  }

...

Angular: My RSVPs (Profile)

We now have an API endpoint providing a list of upcoming events a user has responded to. Let's make a My RSVPs (profile) component to display this information to the authenticated user.

Create a My RSVPs Component

First, we'll generate our new component. Run the following command:

$ ng g component pages/my-rsvps

We now have our My RSVPs component scaffolded.

Update the App Routing Module

The My RSVPs component is a routed component, so let's add it to our app-routing.module.ts:

// src/app/core/app-routing.module.ts
...
import { MyRsvpsComponent } from './pages/my-rsvps/my-rsvps.component';

const routes: Routes = [
  ...,
  {
    path: 'my-rsvps',
    component: MyRsvpsComponent,
    canActivate: [
      AuthGuard
    ]
  },
  ...
];
...

The user must be authenticated in order to have a user ID and stored RSVPs, so we'll implementAuthGuard for this route.

Add Capitalize Utility to Service

We're going to display the identity provider (IdP) that the user is currently logged in with. In order to display this from the data given by the user's ID, we'll create a small utility method that capitalizes the first letter of a string.

Open the utils.service.ts file and add this method:

// src/app/core/utils.service.ts
...
  capitalize(str: string): string {
    // Capitalize first letter of string
    return str.charAt(0).toUpperCase() + str.slice(1);
  }
...

My RSVPs Component Class

Let's implement our My RSVPs component class. Open the my-rsvps.component.ts file and add:

// src/app/pages/my-rsvps/my-rsvps.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { AuthService } from './../../auth/auth.service';
import { ApiService } from './../../core/api.service';
import { UtilsService } from './../../core/utils.service';
import { FilterSortService } from './../../core/filter-sort.service';
import { Subscription } from 'rxjs/Subscription';
import { EventModel } from './../../core/models/event.model';

@Component({
  selector: 'app-my-rsvps',
  templateUrl: './my-rsvps.component.html',
  styleUrls: ['./my-rsvps.component.scss']
})
export class MyRsvpsComponent implements OnInit, OnDestroy {
  pageTitle = 'My RSVPs';
  eventListSub: Subscription;
  eventList: EventModel[];
  loading: boolean;
  error: boolean;
  userIdp: string;

  constructor(
    private title: Title,
    public auth: AuthService,
    private api: ApiService,
    public fs: FilterSortService,
    public utils: UtilsService) { }

  ngOnInit() {
    this.title.setTitle(this.pageTitle);
    this.userIdp = this._getIdp;
    this._getEventList();
  }

  private _getEventList() {
    this.loading = true;
    // Get events user has RSVPed to
    this.eventListSub = this.api
      .getUserEvents$(this.auth.userProfile.sub)
      .subscribe(
        res => {
          this.eventList = res;
          this.loading = false;
        },
        err => {
          console.error(err);
          this.loading = false;
          this.error = true;
        }
      );
  }

  private get _getIdp(): string {
    const sub = this.auth.userProfile.sub.split('|')[0];
    let idp = sub;

    if (sub === 'auth0') {
      idp = 'Username/Password';
    } else if (idp === 'google-oauth2') {
      idp = 'Google';
    } else {
      idp = this.utils.capitalize(sub);
    }
    return idp;
  }

  ngOnDestroy() {
    this.eventListSub.unsubscribe();
  }

}

We'll use our standard imports for routed components with an API call, as well as theFilterSortService to order the events by date. Then we'll add our standard properties to manage page title, the event list subscription, etc. We'll also add a userIdp property.

In our ngOnInit() method, we'll set the page title and _getEventList(), which subscribes to the getUserEvents$() observable we created earlier, passing the user's ID (theauth.userProfile.sub property) to the API endpoint.

Our _getIdp() accessor gets the identity provider from the user's account ID. The userProfile.sub account IDs look something like this:

google-oauth2|23C94879435023998476321
twitter|34B23492010786950049439
auth0|09C3764109863877665210

They are strings with the identity provider followed by a pipe | and then a string of alphanumeric characters. In order to display the user's IdP in a friendly way, we'll split() on the pipe and then treat the IdP to make it more readable, if necessary.

Finally, we'll unsubscribe from our API observable in the ngOnDestroy()method.

My RSVPs Component Template

Open the my-rsvps.component.html template file:

<!-- src/app/pages/my-rsvps/my-rsvps.component.html -->
<h1 class="text-center">{{pageTitle}}</h1>
<p class="lead" *ngIf="auth.loggedIn">
  Hello, <strong [innerHTML]="auth.userProfile.name"></strong>! You logged in with {{userIdp}}.
  <ng-template [ngIf]="auth.isAdmin">
    You may <a routerLink="/admin">create and administer events</a>.
  </ng-template>
</p>

<app-loading *ngIf="loading"></app-loading>

<ng-template [ngIf]="utils.isLoaded(loading)">
  <ng-template [ngIf]="eventList">
    <!-- Event list retrieved but no RSVPs yet -->
    <p *ngIf="!eventList.length" class="lead">
      You have not RSVPed to any events yet. Check out the <a routerLink="/">homepage</a> to see a list of upcoming events.
    </p>

    <ng-template [ngIf]="eventList.length">
      <p class="lead">You have <strong>RSVPed</strong> for the following upcoming events:</p>

      <!-- Events listing -->
      <div class="list-group">
        <a
          *ngFor="let event of fs.orderByDate(eventList, 'startDatetime')"
          [routerLink]="['/event', event._id]"
          [queryParams]="{tab: 'rsvp'}"
          class="list-group-item list-group-item-action flex-column align-items-start">
          <div class="d-flex w-100 justify-content-between">
            <h5 class="mb-1" [innerHTML]="event.title"></h5>
            <small>{{utils.eventDates(event.startDatetime, event.endDatetime)}}</small>
          </div>
          <small class="mb-1">Click to view or update this RSVP</small>
        </a>
      </div>
    </ng-template>
  </ng-template>

  <!-- Error loading events -->
  <p *ngIf="error" class="alert alert-danger">
    <strong>Oops!</strong> There was an error getting your RSVP data.
  </p>
</ng-template>

We'll ensure the user is logged in, welcome them by name, and display the IdP they logged in with. If the user has admin privileges, we'll show a message with a link to the Admin page where they can create and administer events.

We'll then check whether data has been loaded from the API. If the user hasn't RSVPed to any events yet, we'll show a message letting them know to check out the events listed on the homepage.

If events are present, we'll show a listing with titles, dates, and links to each event's RSVP tab. This way the user can easily view or update their RSVP.

Last, we'll show an error if there was a problem retrieving data from the API.

The My RSVPs component should now look like this in the browser:

Angular app - My RSVPs page component

Update Header Component

We have a route for our My RSVPs component, but no links to it in the application. Let's add some in our header.component.html:

<!-- src/app/header/header.component.html -->
<header id="header" class="header">
  ...
      <span *ngIf="auth.loggedIn">
        <a routerLink="/my-rsvps">{{auth.userProfile?.name}}</a>
  ...     
  <nav id="nav" class="nav" role="navigation">
    <ul class="nav-list">
      ...
      <li>
        <a
          *ngIf="auth.loggedIn"
          routerLink="/my-rsvps"
          routerLinkActive="active">My RSVPs</a>
      </li>
      ...
    </ul>
  </nav>
...

We'll link the authenticated user's name to the /my-rsvps route. We'll also add a link in the navigation sidebar. This link should only appear if the user is logged in.

Angular: Renew Tokens With Auth0

You may have noticed throughout development that your access token periodically expires if the same session is left open for longer than two hours. This can result in unexpected loss of access to the API, or the UI still displaying elements that aren't actually accessible to unauthenticated users.

In order to prevent session disruption, we're going to implement automatic authentication renewal with Auth0. The auth0.js library has a method for performing silent authentication to acquire new tokens.

Important Note: If you are using Auth0 social connections in your app, please make sure that you have set the connections up to use your own client app keys. If you're using Auth0 dev keys, token renewal will always return login_required. Each social connection's details has a link with explicit instructions on how to acquire your own key for the particular IdP.

Token renewal with silent authentication will not reload our app or redirect users to the hosted Auth0 login page. The renewal will take place behind the scenes in an iframe, preventing disruption of the user experience.

Add a Silent Callback HTML File

In order to handle the silent renewal in the iframe, we need a static callback page that lives outside our Angular app.

Let's create a new file in the root of our project called silent.html and add the following code:

<!-- silent.html -->
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.auth0.com/js/auth0/8.7/auth0.min.js"></script>
  <script>
    var AUTH0_CLIENT_ID = '[YOUR_CLIENT_ID]';
    var AUTH0_DOMAIN = '[YOUR_DOMAIN]'; // e.g., kmaida.auth0.com

    var webAuth = new auth0.WebAuth({
      clientID: AUTH0_CLIENT_ID,
      domain: AUTH0_DOMAIN,
      scope: 'openid profile',
      responseType: 'token id_token',
      redirectUri: 'http://localhost:4200'
    });

    webAuth.parseHash(window.location.hash, function (err, response) {
      parent.postMessage(err || response, 'http://localhost:4200');
    });
  </script>
</head>
<body></body>
</html>

When we call the auth0.js renewAuth() method, this is the page that we want to redirect to after the token is renewed in the iframe. Change [YOUR_CLIENT_ID]and [YOUR_DOMAIN] to the appropriate information for your Auth0 Client. We can then send the postMessage back to the parent (the Angular app).

Note: When we're ready to deploy our app to production, we'll need to change the redirectUri to our production URL.

Update Server to Serve a Silent Callback File

Now we have a static silent.html page, but there's no way to access it. Let's update our server.js to serve this static file when it's requested:

// server.js
...
/*
 |--------------------------------------
 | App
 |--------------------------------------
 */

...

// Serve static silent.html file at /silent
app.use('/silent', express.static(path.join(__dirname, './silent.html')));

// Set static path to Angular app in dist
...

Important Note: We need to make sure we place the app.use('/silent... line above the static path that serves the Angular ./dist folder in the App block. This will ensure that it is looked up first by Express.

If you visit this page in the browser at http://localhost:8083/silent.html, the proper file should be sent. The page should appear blank, so review the developer tools to make sure it was served as expected.

Update Auth Config

We'll need a new redirect configuration variable to use with our renewAuth()call. Open the auth.config.ts file and add it like so:

// src/app/auth/auth.config.ts
...

interface AuthConfig {
  ...
  SILENT_REDIRECT: string;
  ...
};

export const AUTH_CONFIG: AuthConfig = {
  ...,
  SILENT_REDIRECT: `${ENV.BASE_URI}/silent`,
  ...
};

Now we can access this data along with all of our other front-end Auth0 configuration settings.

Update Auth Service to Support Token Renewal

Now we'll make updates to our AuthService to support scheduled, silent token renewal.

Open the auth.service.ts file and let's get started:

// src/app/core/auth/auth.service.ts
...
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class AuthService {
  ...
  // Subscribe to token expiration stream
  refreshSub: Subscription;

  constructor(private router: Router) {
    // If authenticated, set local profile property,
    // admin status, update login status, schedule renewal.
    // If not authenticated but there are still items
    // in localStorage, log out.
    ...
    if (this.tokenValid) {
      ...
      this.scheduleRenewal();
    } else if ...
  }

  ...

  private _setSession(authResult, profile?) {
    // Set tokens and expiration in localStorage
    ...
    // If initial login, set profile and admin information
    if (profile) {
      localStorage.setItem('profile', JSON.stringify(profile));
      this.userProfile = profile;
      this.isAdmin = this._checkAdmin(profile);
      localStorage.setItem('isAdmin', this.isAdmin.toString());
    }
    // Update login status in loggedIn$ stream
    ...
    // Schedule access token renewal
    this.scheduleRenewal();
  }

  ...

  logout(noRedirect?: boolean) {
    ...
    // Unschedule access token renewal
    this.unscheduleRenewal();
    // Return to homepage
    if (noRedirect !== true) {
      this.router.navigate(['/']);
    }
  }

  ...

  renewToken() {
    this._auth0.renewAuth({
      redirectUri: AUTH_CONFIG.SILENT_REDIRECT,
      usePostMessage: true
    }, (err, authResult) => {
      if (authResult && authResult.accessToken) {
        this._setSession(authResult);
      } else if (err) {
        console.warn(`Could not renew token: ${err.errorDescription}`);
        // Log out without redirecting to clear auth data
        this.logout(true);
        // Log in again
        this.login();
      }
    });
  }

  scheduleRenewal() {
    // If user isn't authenticated, do nothing
    if (!this.tokenValid) { return; }
    // Unsubscribe from previous expiration observable
    this.unscheduleRenewal();
    // Create and subscribe to expiration observable
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    const expiresIn$ = Observable.of(expiresAt)
      .flatMap(
        expires => {
          const now = Date.now();
          // Use timer to track delay until expiration
          // to run the refresh at the proper time
          return Observable.timer(Math.max(1, expires - now));
        }
      );

    this.refreshSub = expiresIn$
      .subscribe(
        () => {
          this.renewToken();
          this.scheduleRenewal();
        }
      );
  }

  unscheduleRenewal() {
    if (this.refreshSub) {
      this.refreshSub.unsubscribe();
    }
  }

}

First, we'll import Observable and Subscription to support creating an observable token expiration timer.

We'll declare a new property for a subscription to a token expiration stream we'll create shortly. This property is called refreshSub and has a type of Subscription.

In the constructor() method, we already check to see if the user already has a valid token withthis.tokenValid. This accounts for persistent login and can occur if the user leaves our app and then returns before their access token has expired. In this case, we also want to schedule a renewal of their access token. This will be implemented with a new scheduleRenewal() method, so let's call this function in the constructor.

Next, we'll update the _setSession() method. We'll make the profileargument optional by adding a question mark. We'll do this because we'll be calling this method when tokens have been successfully renewed with silent authentication, but the same user is logging in so we won't need to set all the profile information again. It will simply persist from the previous session. We can then wrap anything related to setting profile data in an if statement that checks to see if a profile argument has been passed to the method. We also need to call scheduleRenewal() to restart the expiration timer when a new token has been retrieved.

The logout() method will now support an optional argument: noRedirect. This way, we can call logout()without being redirected back to the homepage. This is useful if our silent authentication encounters an error. In such a case, we want the app to clean up all existing authentication data before prompting the user to log in again. We'll check that the noRedirect parameter is not equal to true before redirecting.

Now we'll create three new methods to implement token renewal. The first method is renewToken():

  renewToken() {
    this._auth0.renewAuth({
      redirectUri: AUTH_CONFIG.SILENT_REDIRECT,
      usePostMessage: true
    }, (err, authResult) => {
      if (authResult && authResult.accessToken) {
        this._setSession(authResult);
      } else if (err) {
        console.warn(`Could not renew token: ${err.errorDescription}`);
        // Log out without redirecting to clear auth data
        this.logout(true);
        // Log in again
        this.login();
      }
    });
  }

This method uses auth0.js to acquire new tokens using the renewAuth()method on our existing_auth0 web auth instance. We'll set redirectUri to the silent redirect page. We'll also set theusePostMessage: true option. This uses postMessage to implement cross-origin communication between our parent app and the silent authentication taking place in an iframe. On successful acquisition of a new access token, we'll call _setSession() to start a new session. If an error occurs, we'll execute logout(true) (without redirection) to quietly clear authentication data, then we'll prompt the user to log in again.

Note: As written, the hosted login redirect will happen automatically without forewarning the user. Keep in mind that this only occurs if silent authentication fails for some reason. If you'd like to notify the user before automatically prompting them to log in again or give them a choice to stay logged out and return to the homepage, you should do so here.

The next new method is scheduleRenewal():

  scheduleRenewal() {
    // If user isn't authenticated, do nothing
    if (!this.tokenValid) { return; }
    // Unsubscribe from previous expiration observable
    this.unscheduleRenewal();
    // Create and subscribe to expiration observable
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    const expiresIn$ = Observable.of(expiresAt)
      .flatMap(
        expires => {
          const now = Date.now();
          // Use timer to track delay until expiration
          // to run the refresh at the proper time
          return Observable.timer(Math.max(1, expires - now));
        }
      );

    this.refreshSub = expiresIn$
      .subscribe(
        () => {
          this.renewToken();
          this.scheduleRenewal();
        }
      );
  }

If the user is authenticated, we'll do a cleanup check and then create an observable calledexpiresIn$. We'll use the flatMap() RxJS operator to flatten the stream. We'll then utilize anObservable.timer() that produces a value (0) when the current access token expires.

Then we'll subscribe to the expiresIn$ observable. As declared with our properties earlier, this subscription is called refreshSub. When the 0 value is produced indicating the token is expired, we'll call renewToken() and scheduleRenewal() to set the session and reset the timer with the fresh token's expiration countdown.

The final new method is unscheduleRenewal():

  unscheduleRenewal() {
    if (this.refreshSub) {
      this.refreshSub.unsubscribe();
    }
  }

This method simply checks for the existence of a refreshSub subscription and unsubscribes from it. The method is called on log out, or if we need to subscribe to a new token expiration observable (expiresIn$).

Update Auth0 Client Settings

In order to avoid getting a 403 Forbidden error when silently renewing authentication, we need to make sure our Auth0 Client's callback settings are updated to allow our new /silent route.

Go to your Auth0 Dashboard Clients section and select your app's Client. In the Allowed Callback URLs, add http://localhost:8083/silent to the list. Save your changes.

Note: Recall that we are using the loggedIn property to track authentication state in templates (rather than auth.tokenValid). This is ideal because loggedIn is updated on the fly as the state changes. We also won't get a flash of a logged-out state during a successful token renewal.

Test Token Expiration and Renewal

For testing, you can change the access token expiration time in your Auth0 Dashboard APIs. Select the API you set up for this application. You can then change the Token Expiration For Browser Flows (Seconds) value. It is 7200seconds by default (2 hours). For testing token renewal, you can change this value to something much shorter. Test it out with the setting at 30 seconds. If you open your browser's Network panel, your login should be persisted and you should see activity every 30 seconds indicating successful silent token renewal.

Auth0 dashboard APIs change token expiration

Note: Make sure to change the token expiration back once you're finished testing your renewal implementation.

Summary

In Part 7 of our Real-World Angular Series, we've covered deleting events, listing events a user has RSVPed to, and silent renewal of authentication tokens. In the final part of the tutorial series, we'll cover NgModule refactoring, lazy loading, and production deployment with SSL.

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
web dev ,angular ,web application development ,tokens

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}