developers

NgRx Authentication Tutorial: Secure an App

Learn how to use CLI schematics, effects, Auth0, and more to secure your NgRx application.

TL;DR: In this article, we’ll get a quick refresher on NgRx basics and get up to speed on more features of the NgRx ecosystem. We'll then walk through how to add Auth0 authentication to an NgRx app. You can access the finished code for this tutorial on the ngrx-auth GitHub repository.

Adding Authentication with Auth0

In this section, we're going to set up Auth0, create an Angular authentication service, and wire everything up using NgRx Effects. The Auth0 log in screen will look like this:

Auth0 default login screen

Sign Up for Auth0

The first thing you'll need to do is sign up for an Auth0 account to manage authentication. You can sign up for a free Auth0 account here. (If you've already got an account, great! You can simply log in to Auth0.)

Set Up an Application

Once you've got your account, you can set up an application to use with our NgRx project. We'll only be setting up a Single Page Application (SPA) in Auth0 since we're using the Google Books API as our back end.

Here's how to set that up:

  1. Go to your Auth0 Applications and click the "Create Application" button.
  2. Name your new app, select "Single Page Web Applications," and click the "Create" button. You can skip the Quick Start and click on Settings.
  3. In the Settings for your new Auth0 app, add
    http://localhost:4200/callback
    to the Allowed Callback URLs. (We're using
    localhost:4200
    since it's the default port for the Angular CLI
    serve
    command.)
  4. Add
    http://localhost:4200
    to both Allowed Web Origins and Allowed Logout URLs.
  5. Click the "Save Changes" button.
  6. Copy down your Domain and Client ID. We'll use them in just a minute.
  7. If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter.

Note: Under the OAuth tab of Advanced Settings (at the bottom of the Settings section) you should see that the JsonWebToken Signature Algorithm is set to

RS256
. This is the default for new applications. If it is set to
HS256
, please change it to
RS256
. You can read more about RS256 vs. HS256 JWT signing algorithms here.

Install auth0.js and Set Up Environment Config

Now that we've got the SPA authentication set up, we need to add the JavaScript SDK that allows us to easily interact with Auth0. We can install that with this command:

npm install auth0-js --save

We'll use this library in just a bit when we create our authentication service. We can now set up our environment variables using the client ID and domain we copied from our Auth0 application settings. The Angular CLI makes this very easy. Open up

/src/environments/environment.ts
and add the
auth
section below:

// src/environments/environment.ts
export const environment = {
  production: false,
  auth: {
    clientID: 'YOUR-AUTH0-CLIENT-ID',
    domain: 'YOUR-AUTH0-DOMAIN', // e.g., you.auth0.com
    redirect: 'http://localhost:4200/callback',
    scope: 'openid profile email'
  }
};

Note: if we were using an API identifier, we would add an

audience
property to this configuration. We're leaving it out here since we're using the Google Books API.

This file stores the authentication configuration variables so we can re-use them throughout our application by importing them. Be sure to update the

YOUR-AUTH0-CLIENT-ID
and
YOUR-AUTH0-DOMAIN
to your own information from your Auth0 application settings.

Add Authentication Service

We're now ready to set up the main engine of authentication for our application: the authentication service. The authentication service is where we’ll handle interaction with the Auth0 library. It will be responsible for anything related to getting the token from Auth0, but won’t dispatch any actions to the store.

To generate the service using the CLI, run:

ng g service auth/services/auth --no-spec

We first need to import the

auth0-js
library, our environment configuration, and
bindNodeCallback
from RxJS:

// src/app/auth/services/auth.service.ts
// Add these to the imports
import { Injectable } from '@angular/core';
import { bindNodeCallback } from 'rxjs';
import * as auth0 from 'auth0-js';
import { environment } from '../../../environments/environment';

// ...below code remains the same

Next, we need to set some properties on our class. We'll need an Auth0 configuration property, a flag we'll use for setting a property in local storage to persist our authentication on refresh, and some URLs for navigation. We'll also add a property for setting the token expiration date. Add these before the class

constructor
:

// src/app/auth/services/auth.service.ts
// ...previous code remains the same

// Add properties above the constructor
private _Auth0 = new auth0.WebAuth({
    clientID: environment.auth.clientID,
    domain: environment.auth.domain,
    responseType: 'token',
    redirectUri: environment.auth.redirect,
    scope: environment.auth.scope
  });
// Track whether or not to renew token
private _authFlag = 'isLoggedIn';
// Authentication navigation
private _onAuthSuccessUrl = '/home';
private _onAuthFailureUrl = '/login';
private _logoutUrl = 'http://localhost:4200';
private _expiresAt: number;

// ...below code remains the same

We're setting the different URLs here in the service in case multiple places in the application need to perform this redirection.

Next, we'll need to set up two observables using the Auth0 library that we can access with our NgRx effects later on. The Auth0 library uses Node-style callback functions. Luckily, we can use

bindNodeCallback
, built into RxJS, to transform these functions into observables. Add these lines after the properties we just created but before the constructor:

// src/app/auth/services/auth.service.ts
// ...above code unchanged
// Create observable of Auth0 parseHash method to gather auth results
parseHash$ = bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0));

// Create observable of Auth0 checkSession method to
// verify authorization server session and renew tokens
checkSession$ = bindNodeCallback(this._Auth0.checkSession.bind(this._Auth0));
// ...below code unchanged

The first observable we've created is using Auth0's

parseHash
function, which will parse the access token from the window hash fragment. We'll use this during
LoginComplete
. The second observable is for the
checkSession
function, which we'll use when the app refreshes during
CheckLogin
. You'll notice that both of these have a little bit of JavaScript magic tacked onto them. We need to
bind
the Auth0 object to both of these functions for them to work correctly. This is pretty common when using
bindNodeCallback
.

We'll now need functions to handle logging in, setting the authentication, and logging out, as well as a public getters for our URLs and whether we're logged in. We'll use a flag in local storage to track that the application is authenticated. That way we can check for that flag and call

checkSession
to update our state.

Update the service like so after the constructor:

// src/app/auth/services/auth.service.ts
// ...previous code remains the same
// Add these functions after the constructor
get authSuccessUrl(): string {
  return this._onAuthSuccessUrl;
}

get authFailureUrl(): string {
  return this._onAuthFailureUrl;
}

get authenticated(): boolean {
  return JSON.parse(localStorage.getItem(this._authFlag));
}

resetAuthFlag() {
  localStorage.removeItem(this._authFlag);
}

login() {
  this._Auth0.authorize();
}

setAuth(authResult) {
  this._expiresAt = authResult.expiresIn * 1000 + Date.now();
  // Set flag in local storage stating this app is logged in
  localStorage.setItem(this._authFlag, JSON.stringify(true));
}

logout() {
  // Set authentication status flag in local storage to false
  localStorage.setItem(this._authFlag, JSON.stringify(false));
  // This does a refresh and redirects back to homepage
  // Make sure you have the logout URL in your Auth0
  // Dashboard Application settings in Allowed Logout URLs
  this._Auth0.logout({
    returnTo: this._logoutUrl,
    clientID: environment.auth.clientID
  });
}

// ...below code remains the same

Auth0 is handling logging in and logging out, and we have a

setAuth
method to set the local storage flag. If we needed to return the token for use in the store, we'd do that here. We're going to handle much of the flow of our authentication using effects. Before we set those up, though, we're going to need some components.

Build Out the Authentication UI

We've got the authentication service set up, but now we need to build out our UI. We'll need components for logging in, a callback component for Auth0 to redirect to, a logout dialog, and a logout button on our user home screen. We'll also need to add some routing, and we'll want to add a route guard to lock down our

home
route and redirect users back to the
login
route if no token is found.

Log In Components

We need to create a

login
route with a simple form that contains a button to log in. This button will dispatch the
Login
action. We'll set up an effect in just a bit that will call the authentication service.

First, create a container component called

login-page
:

ng g c auth/components/login-page -m auth --no-spec

Replace the boilerplate with this:

// src/app/auth/components/login-page.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromStore from '@app/state';
import { Login } from '@app/auth/actions/auth.actions';

@Component({
  selector: 'abl-login-page',
  template: `
    <abl-login-form
      (submitted)="onLogin($event)">
    </abl-login-form>
  `,
  styles: [
    `
      :host {
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 128px 12px 12px 12px;
      }

      abl-login-form {
        width: 100%;
        min-width: 250px;
        max-width: 300px;
      }
    `
  ]
})
export class LoginPageComponent implements OnInit {
  constructor(private store: Store<fromStore.State>) {}

  ngOnInit() {}

  onLogin() {
    this.store.dispatch(new Login());
  }
}

Notice that we'll be passing the

onLogin
function into our form, which will dispatch our action.

If you see an error when running the application right now, it's because we haven't yet created

abl-login-form
referenced in the template. Let's create that component now:

ng g c auth/components/login-form -m auth --no-spec

Now replace the contents of that file with this:

// src/app/auth/components/login-form.component.ts
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'abl-login-form',
  template: `
    <mat-card>
      <mat-card-title>
        Welcome
      </mat-card-title>
      <mat-card-content>
        <form [formGroup]="loginForm" (ngSubmit)="submit()">
          <div class="loginButtons">
            <button type="submit" mat-button>Log In</button>
          </div>
        </form>
      </mat-card-content>
    </mat-card>
  `,
  styles: [
    `
      :host {
        width: 100%;
      }

      form {
        display: flex;
        flex-direction: column;
        width: 100%;
      }

      mat-card-title,
      mat-card-content {
        display: flex;
        justify-content: center;
      }

      mat-form-field {
        width: 100%;
        margin-bottom: 8px;
      }

      .loginButtons {
        display: flex;
        flex-direction: row;
        justify-content: flex-end;
      }
    `
  ]
})
export class LoginFormComponent implements OnInit {
  @Output() submitted = new EventEmitter<any>();

  loginForm = new FormGroup({});

  ngOnInit() {}

  submit() {
    this.submitted.emit();
  }
}

Finally, in our

AuthRoutingModule
(
auth/auth-routing.module.ts
), import
LoginPageComponent
at the top of the file and add a new route to the routes array, above the
home
route:

// src/app/auth/auth-routing.module.ts
// Add to imports at the top
import { LoginPageComponent } from '@app/auth/components/login-page.component';
// ...
// Add to routes array above the home route
{ path: 'login', component: LoginPageComponent },
// ...below code remains the same

We should be able to build the application, navigate to

http://localhost:4200/login
, and see the new form.

Example view of non-functional login form for our NgRx application

Of course, nothing will happen when we click the button, because we don't have any effects listening for the

Login
action yet. Let's finish building our UI and then come back to that.

Add Callback Component

Once Auth0 successfully logs us in, it will redirect back to our application

callback
route, which we'll add in this section. First, let's build the
CallbackComponent
and have it dispatch a
LoginComplete
action.

First, generate the component:

ng g c auth/components/callback -m auth --nospec

This component will just display a loading screen and dispatch

LoginComplete
using
ngOnInit
. Replace the contents of the generated file with this code:

// src/app/auth/components/callback.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromStore from '@app/state';
import { LoginComplete } from '@app/auth/actions/auth.actions';

@Component({
  selector: 'abl-callback',
  template: `
    <p>
      Loading...
    </p>
  `,
  styles: []
})
export class CallbackComponent implements OnInit {
  constructor(private store: Store<fromStore.State>) {}

  ngOnInit() {
    this.store.dispatch(new LoginComplete());
  }
}

And, finally, import the new

CallbackComponent
into
auth/auth-routing.module.ts
and add a new route after the
login
route:

// src/app/auth/auth-routing.module.ts
// Add to imports at the top
import { CallbackComponent } from '@app/auth/components/callback.component';
// ...
// Add to routes array after the login route
{ path: 'callback', component: CallbackComponent },
// ...below code remains the same

Once again, if we build the application and run it, we're now able to navigate to

http://localhost:4200/callback
and see the new component (which, once again, will do nothing yet).

Log Out Buttons

For logging out of the application, we'll need a confirmation dialog, as well as log out buttons on the user home and books page components.

Let's add the buttons first. In

auth/user-home.component.ts
, add a button in the template under the book collection button. The completed template will look like this:

// src/app/auth/components/user-home.component.ts
<div>
    <h3>Welcome Home!</h3>
       <button mat-button raised color="accent" (click)="goToBooks()">See my book collection</button>
    <button mat-button raised color="accent" (click)="logout()">Log Out</button>
</div>

Then, add these imports at the top of the file:

// src/app/auth/components/user-home.component.ts
import { Store } from '@ngrx/store';
import * as fromStore from '@app/state';
import { Logout } from '@app/auth/actions/auth.actions';
// ...below code remains the same

This will let us add the store to our constructor:

// src/app/auth/components/user-home.component.ts
// ...above code unchanged
constructor(private store: Store<fromStore.State>, private router: Router) {}
// ...below code unchanged

With that done, we can now add a

logout
function to the component that will dispatch the
Logout
action:

// src/app/auth/components/user-home.component.ts
// add anywhere after the constructor
logout() {
    this.store.dispatch(new Logout());
  }
// ...other code unchanged

We can do something similar with the

BooksPageComponent
so the user can log out from their book collection. In
books/components/books-page-component.ts
, add the following block underneath the
mat-card-title
tag:

// src/app/books/components/books-page.component.ts
<mat-card-actions>
  <button mat-button raised color="accent" (click)="logout()">Logout</button>
</mat-card-actions>

Here's what the completed

template
for the
BooksPageComponent
will look like:

// src/app/books/components/books-page.component.ts
<mat-card>
  <mat-card-title>My Collection</mat-card-title>
  <mat-card-actions>
    <button mat-button raised color="accent" (click)="logout()">Logout</button>
  </mat-card-actions>
</mat-card>
<abl-book-preview-list [books]="books$ | async"></abl-book-preview-list>

Next, add the

Logout
action to the imports:

// src/app/books/components/books-page.component.ts
import { Logout } from '@app/auth/actions/auth.actions';
// ...remaining code unchanged

And, finally, add a

logout
function to dispatch the
Logout
action from the button:

// src/app/books/components/books-page.component.ts
// add anywhere after the constructor
logout() {
  this.store.dispatch(new Logout());
}
// ...remaining code unchanged

And that's it! Now we just need to add the logout confirmation.

Log Out Prompt

We're going to use Angular Material to pop up a confirmation when the user clicks log out.

To generate the component, run:

ng g c auth/components/logout-prompt -m auth --no-spec

Then, replace the contents of the file with this:

// src/app/auth/components/logout-prompt.component.ts
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material';

@Component({
  selector: 'abl-logout-prompt',
  template: `
    <h3 mat-dialog-title>Log Out</h3>

    <mat-dialog-content>
      Are you sure you want to log out?
    </mat-dialog-content>

    <mat-dialog-actions>
      <button mat-button (click)="cancel()">
        No
      </button>
      <button mat-button (click)="confirm()">
        Yes
      </button>
    </mat-dialog-actions>
  `,
  styles: [
    `
      :host {
        display: block;
        width: 100%;
        max-width: 300px;
      }

      mat-dialog-actions {
        display: flex;
        justify-content: flex-end;
      }

      [mat-button] {
        padding: 0;
      }
    `
  ]
})
export class LogoutPromptComponent {
  constructor(private ref: MatDialogRef<LogoutPromptComponent>) {}

  cancel() {
    this.ref.close(false);
  }

  confirm() {
    this.ref.close(true);
  }
}

You're probably seeing an error in the console at this point. That's because there is one thing that the CLI didn't do for us. We need to create an

entryComponents
array in the
NgModule
decorator of
AuthModule
and add the
LogoutPromptComponent
to it.

Entry components are components loaded imperatively through code instead of declaratively through a template. The

LogoutPromptComponent
doesn't get called through a template, it gets loaded by Angular Material when the user clicks the
Log Out
button.

Just add this after the

declarations
array in
auth/auth.module.ts
(don't forget a comma!):

// src/app/auth/auth.module.ts
// ... imports

@NgModule({
  imports: [ ... ],
  declarations: [ ... ],
  entryComponents: [LogoutPromptComponent]
})

// ...other code unchanged

We'll create an effect for

Logout
to open the prompt, listen for the response, and dispatch either
LogoutCancelled
or
LogoutConfirmed
when we wire everything up in just a bit.

Add Route Guard

We've added our login and logout components, but we want to ensure that a visitor to our site can only access the

home
route if they are logged in. Otherwise, we want to redirect them to our new
login
route. We can accomplish this with a
CanActivate
route guard.

To create the route guard, run this command:

ng g guard auth/services/auth --no-spec

This will create

/auth/services/auth.guard.ts
. Replace the contents of this file with the following:

// src/app/auth/services/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { of } from 'rxjs';
import { mergeMap, map, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';

import { AuthService } from '@app/auth/services/auth.service';
import * as fromStore from '@app/state';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private store: Store<fromStore.State>,
    private router: Router
  ) {}

  canActivate() {
    return this.checkStoreAuthentication().pipe(
      mergeMap(storeAuth => {
        if (storeAuth) {
          return of(true);
        }

        return this.checkApiAuthentication();
      }),
      map(storeOrApiAuth => {
        if (!storeOrApiAuth) {
          this.router.navigate(['/login']);
          return false;
        }

        return true;
      })
    );
  }

  checkStoreAuthentication() {
    return this.store.select(fromStore.selectIsLoggedIn).pipe(take(1));
  }

  checkApiAuthentication() {
    return of(this.authService.authenticated);
  }
}

Let's break down what's happening here. When this guard runs, we first call the function

checkStoreAuthentication
, which uses the selector we created to get
isLoggedIn
from our global state. We also call
checkApiAuthentication
, which checks if the state matches
authenticated
on our
AuthService
(which we get from local storage). If these are true, we return true and allow the route to load. Otherwise, we redirect the user to the
login
route.

We'll want to add this route guard to both the

home
route (in our
AuthModule
) and our
books
route (specifically, the
forChild
in
BooksModule
).

In

auth/auth-routing.module.ts
, add the guard to the imports:

// src/app/auth/auth-routing.module.ts
import { AuthGuard } from './services/auth.guard';

Then, modify the

home
route to the following:

{
    path: 'home',
    component: UserHomeComponent,
    canActivate: [AuthGuard]
}

Similarly, import the

AuthGuard
at the top of
books/books.module.ts
:

// src/app/books/books.module.ts
import { AuthGuard } from '@app/auth/services/auth.guard';
// ...all other code unchanged

Then, modify

RouterModule.forChild
to this:

// src/app/books/books.module.ts
// ...above code unchanged
RouterModule.forChild([
  { path: '', component: BooksPageComponent, canActivate: [AuthGuard] },
]),
// ...below code unchanged

We did it! If we run

ng serve
, we should no longer be able to access the
home
or
books
route. Instead, we should be redirected to
login
.

Note that we haven't added any sort of 404 redirecting here. To do that, we'd want to add a wildcard route and a

PageNotFoundComponent
to redirect to. You can read more about wildcard routes in the Angular docs.

Checking Authentication on App Load

We just have one last UI piece to add. We need to dispatch the

CheckLogin
action when the application loads so that we can retrieve the token from the server if the user is logged in. The best place to do this is in the
AppComponent
(
src/app/app.component.ts
).

Our steps in

AppComponent
will be identical to what we did in the
CallbackComponent
— the only difference is the action we will dispatch. In
app.component.ts
, we'll first need to add
OnInit
to our
@angular/core
imports. We'll also need to import the
Store
from
ngrx
, our app
State
, and our
CheckLogin
action. Our complete imports in the file will look like this:

// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromStore from '@app/state';
import { CheckLogin } from '@app/auth/actions/auth.actions';
// below code unchnaged.

Then, in the component class, implement the

OnInit
interface and create the
OnInit
function:

// src/app/app.component.ts
// ...imports and template defined above (unchaged)
// The class definition should look like this:
export class AppComponent implements OnInit {
  constructor() { }

  ngOnInit() {}
}

Lastly, we'll need to inject the

Store
into the constructor and dispatch the
CheckLogin
action inside of
ngOnInit
. The finished class definition should look like this:

// src/app/app.component.ts
// ...imports and template defined above (unchaged)
// The class definition should look like this:
export class AppComponent implements OnInit {
  constructor(private store: Store<fromStore.State>) { }

  ngOnInit() {
    this.store.dispatch(new CheckLogin());
  }
}

You won't notice anything different when you run the application, because we'll be calling

checkSession
on the
AuthService
from an effect. So, let's put it all together by creating effects that will control logging in and out in addition to checking for persisted authentication.

Controlling the Authentication Flow with Effects

Alright, friends, we're ready for the final piece of this puzzle. We're going to add effects to handle our authentication flow. Effects allow us to initiate side effects as a result of actions dispatched in a central and predictable location. This way, if we ever need to universally change the behavior of an action's side effect, we can do so quickly without repeating ourselves.

Add Imports and Update Constructor

All of our effects will go in

auth/effects/auth.effects.ts
, and the CLI has already connected them to our application through the
AuthModule
. All we need to do is fill in our effects.

Before we do that, be sure that all of these imports are at the top of the file:

// auth/effects/auth.effects.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Actions, Effect } from '@ngrx/effects';
import { tap, exhaustMap, map, catchError } from 'rxjs/operators';
import { MatDialog } from '@angular/material';
import * as fromAuth from '../actions/auth.actions';
import { LogoutPromptComponent } from '@app/auth/components/logout-prompt.component';
import { AuthService } from '@app/auth/services/auth.service';
import { of, empty } from 'rxjs';

If you commented out the boilerplate

loadFoos$
effect that the CLI generated earlier, go ahead and delete it:

// src/app/auth/auth.effects.ts
// ...
// Delete both of these lines.
// @Effect()
// loadFoos$ = this.actions$.pipe(ofType(AuthActionTypes.LoadAuths));
// ...

Next, update the constructor so that we're injecting the router, the

AuthService
, and
MatDialog
(from Angular Material):

// auth/effects/auth.effects.ts
constructor(
    private actions$: Actions,
    private authService: AuthService,
    private router: Router,
    private dialogService: MatDialog
  ) { }

We'll use all of these in our effects.

Add Log In Effects

Let's add our log in effects first.

Add the following to our class before the constructor (this is a convention with effects):

// auth/effects/auth.effects.ts
// Add before the constructor
@Effect({ dispatch: false })
login$ = this.actions$.ofType<fromAuth.Login>(fromAuth.AuthActionTypes.Login).pipe(
  tap(() => {
    return this.authService.login();
  })
);

@Effect()
loginComplete$ = this.actions$
  .ofType<fromAuth.Login>(fromAuth.AuthActionTypes.LoginComplete)
  .pipe(
    exhaustMap(() => {
      return this.authService.parseHash$().pipe(
        map((authResult: any) => {
          if (authResult && authResult.accessToken) {
            this.authService.setAuth(authResult);
            window.location.hash = '';
            return new fromAuth.LoginSuccess();
          }
        }),
        catchError(error => of(new fromAuth.LoginFailure(error)))
      );
    })
  );

@Effect({ dispatch: false })
loginRedirect$ = this.actions$
  .ofType<fromAuth.LoginSuccess>(fromAuth.AuthActionTypes.LoginSuccess)
  .pipe(
    tap(() => {
      this.router.navigate([this.authService.authSuccessUrl]);
    })
  );

@Effect({ dispatch: false })
loginErrorRedirect$ = this.actions$
  .ofType<fromAuth.LoginFailure>(fromAuth.AuthActionTypes.LoginFailure)
  .pipe(
    map(action => action.payload),
    tap((err: any) => {
      if (err.error_description) {
        console.error(`Error: ${err.error_description}`);
      } else {
        console.error(`Error: ${JSON.stringify(err)}`);
      }
      this.router.navigate([this.authService.authFailureUrl]);
    })
  );
// ...below code unchanged

Let's break down what's happening in each of these.

  • Login — calls the
    login
    method on
    AuthService
    , which triggers Auth0. Does not dispatch an action.
  • Login Complete — calls
    parseHash$
    on
    AuthService
    , which returns an observable of the parsed hash. If there's a token, this effect calls
    setAuth
    , clears the hash from the window location, and then dispatches the
    LoginSuccess
    action. If there's not a token, the effect dispatches the
    LoginFailure
    action with the error as its payload.
  • Login Redirect — This effect happens when
    LoginSuccess
    is dispatched. It redirects the user to
    home
    (using the
    authSuccessUrl
    property on the
    AuthService
    ) and does not dispatch a new action.
  • Login Error Redirect — This effect happens when
    LoginFailure
    is dispatched. It redirects the user back to
    login
    (using the
    authFailureUrl
    property on the
    AuthService
    ) and does not dispatch a new action.

If we run the application with

ng serve
, we should now be able to successfully log in to our application using Auth0! We'll see a login screen similar to this:

Our Auth0 login screen works!

On your first login, you'll need to click the "Sign Up" button to create a user. Alternatively, you can manually create users from the Auth0 dashboard. In either case, you'll receive an email asking you to verify your email address. Your first login to the application will also trigger an "Authorize App" screen requesting permission. Just click the green button to continue. Once you're all signed up and logged in, you'll be redirected to the

home
route, where you can click the button to view the book collection. Of course, we can't log out yet, so let's add the effects for that now.

Note: You can also use Google to sign up and log in. However, be aware that you will need to generate a Google Client ID and Client Secret in order to complete this tutorial.

Persist Authentication on Refresh

This is great, but if we refresh the application, we'll lose the access token. Because we already set up the flag in local storage and the route guard is checking for that flag, our application will appear to be logged in on refresh. However, we haven't called

checkSession
yet through Auth0, so we'll no longer have the token. Let's add the effect for
CheckLogin
to fix that.

// auth/effects/auth.effects.ts
// add below login effects:
// ...above code unchanged
@Effect()
checkLogin$ = this.actions$.ofType<fromAuth.CheckLogin>(fromAuth.AuthActionTypes.CheckLogin).pipe(
  exhaustMap(() => {
    if (this.authService.authenticated) {
      return this.authService.checkSession$({}).pipe(
        map((authResult: any) => {
          if (authResult && authResult.accessToken) {
            this.authService.setAuth(authResult);
            return new fromAuth.LoginSuccess();
          }
        }),
        catchError(error => {
          this.authService.resetAuthFlag();
          return of(new fromAuth.LoginFailure({ error }));
        })
      );
    } else {
      return empty();
    }
  })
);
// ...below code unchanged

When

CheckLogin
is dispatched, this effect will call
checkSession
on the
AuthService
, which, like
parseHash
, returns token data. If there's token data, the effect will call the
setAuth
method and dispatch the
LoginSuccess
action. If there's an error, the effect will dispatch
LoginFailure
. Those actions will work the same way as with logging in - navigating to
home
on success or
login
on failure.

Let's check if it works. Run

ng serve
, navigate to
http://localhost:4200
, and log in. Once we're back at the
home
route, refresh the page with our browser. We should not be redirected back to the
login
route, but remain on the
home
route. Awesome!

Here's a challenge for you: how might you update this feature so that you can persist the previous route, too? That way a user would land back on

books
when refreshing.

Add Log Out Effects

Let's add our final two effects to finish off this application.

// auth/effects/auth.effects.ts
// Add under the login effects
// ...above code unchanged
@Effect()
logoutConfirmation$ = this.actions$.ofType<fromAuth.Logout>(fromAuth.AuthActionTypes.Logout).pipe(
    exhaustMap(() =>
      this.dialogService
        .open(LogoutPromptComponent)
        .afterClosed()
        .pipe(
          map(confirmed => {
            if (confirmed) {
              return new fromAuth.LogoutConfirmed();
            } else {
              return new fromAuth.LogoutCancelled();
            }
          })
        )
    )
);

@Effect({ dispatch: false })
logout$ = this.actions$
    .ofType<fromAuth.LogoutConfirmed>(fromAuth.AuthActionTypes.LogoutConfirmed)
    .pipe(tap(() => this.authService.logout()));
// ...below code unchanged

Here's what's happening here:

  • Logout Confirmation — This effect will display the log out confirmation dialog. It will then process the result by dispatching either the
    LogoutConfirmed
    or
    LogoutCancelled
    actions.
  • Logout — This effect happens after
    LogoutConfirmed
    has been dispatched. It will call the
    logout
    function on the
    AuthService
    , which tells Auth0 to log us out and redirect back home. This effect does not dispatch another action.

Running

ng serve
again should now allow us to log in, view the book collection, and log out. Be sure to check if we can also log out from the
home
route!

Finished NgRx logout popup

Remember, you can access the finished code for this tutorial here.

Review and Where to Go From Here

Congratulations on making it to the end! We've covered a lot of ground here, like:

  • Using @ngrx/schematics to quickly generate new code
  • Defining global authentication state
  • Using selectors to access authentication state
  • Setting up Auth0 to handle your authentication
  • Using effects to handle the login and logout flows
  • Persisting authentication on refresh

We've spent time laying the foundation of a basic authentication flow in a very simple application. However, we could easily apply this same setup to a much more complex application. Scaling the setup is very minor, and adding new pieces of state, new actions, or new side effects would be relatively easy. We've got all the building blocks in place.

For example, let's say you needed to add the access token to outgoing HTTP requests as an

Authorization
header. You've already got what you need to get this working quickly. You could add
tokenData
to the authentication state and create a selector for it. Then, add the token data as the
payload
of the
LoginSuccess
action and update the effects that use it. Once that's all set up, you could then create an HTTP interceptor to select the token data from the
store
and add it to outgoing requests.

Now that the foundation of the authentication flow has been built, the sky is the limit for how you want to extend it. My goal for this tutorial was to keep it simple while helping you understand some new, fairly complex concepts. I hope you can take this knowledge and use it in the real world — let me know how it goes!