Authenticating Firebase and Angular with Auth0: Part 1

Kim Maida
Share

This article was originally published on the Auth0.com blog, and is republished here with permission.

In this two-part tutorial series, we’ll learn how to build an application that secures a Node back end and an Angular front end with Auth0 authentication. Our server and app will also authenticate a Firebase Cloud Firestore database with custom tokens so that users can leave realtime comments in a secure manner after logging in with Auth0.

The Angular application code can be found at the angular-firebase GitHub repo and the Node API can be found in the firebase-auth0-nodeserver repo.

Authenticating Firebase and Angular with Auth0: Part 1

Part 1 of our tutorial will cover:

  1. Firebase and Auth0
  2. What We’ll Build
  3. Angular CLI
  4. Auth0 Client and API
  5. Firebase Project with Service Account
  6. Node API
  7. Set Up Angular App
  8. Angular App Architecture
  9. Implement Shared Modules
  10. Implement Routing and Lazy Loaded Modules
  11. Loading and Error Components
  12. Authentication Logic
  13. Core Logic
  14. Next Steps

Firebase and Auth0

Firebase is a mobile and web application development platform. Firebase was acquired by Google in 2014, and continues to be developed under the Google umbrella. Firebase provides NoSQL databases (RTDB, or Realtime Database and Cloud Firestore, in beta at the time of writing) hosted in the cloud and connected using web sockets to provide realtime capabilities to apps.

Auth0 is a cloud-based platform that provides authentication and authorization as a service. As an authentication provider, Auth0 enables developers to easily implement and customize login and authorization security for their apps.

Choosing Auth0 + Firebase Authentication

If you’re already familiar with Firebase’s offerings, you might be asking: why would we implement Auth0 with custom tokens in Firebase instead of sticking with Firebase’s built-in authentication by itself?

Firstly, there is an important distinction to make here. Using Auth0 to secure Firebase does not mean you are not using Firebase auth. Firebase has a custom authentication approach that allows developers to integrate their preferred identity solution with Firebase auth. This approach enables developers to implement Firebase auth so that it functions seamlessly with proprietary systems or other authentication providers.

There are many potential reasons we might want to integrate Auth0 with Firebase authentication. Alternatively, there are scenarios where using basic Firebase auth by itself could suffice. Let’s explore.

You can use Firebase’s built-in authentication by itself if you:

  • Only want to authenticate Firebase RTDB or Firestore and have no need to authenticate additional back ends
  • Only need a small handful of login options and do not need enterprise identity providers, integration with your own user storage databases, etc.
  • Do not need extensive user management, profile enrichment, etc. and are comfortable managing users strictly through an API
  • Have no need to customize authentication flows
  • Do not need to adhere to compliance regulations regarding the storage of user data.

You should consider Auth0 with a custom Firebase token if you:

Essentially, Firebase’s basic authentication providers should suffice if you have a very simple app with bare-bones authentication needs and are only using Firebase databases. However, should you need more than that, Firebase offers a great way to use their services with other authentication solutions. This is a much more realistic scenario that many developers will be faced with, so we’ll explore it in detail here.

What We’ll Build

We’re going to build a Node.js API secured with Auth0 that mints custom Firebase tokens and also returns data on ten different dog breeds.

We’ll also build an Angular front end app called “Popular Dogs” that displays information about the ten most popular dogs in 2016, ranked by public popularity by the American Kennel Club (AKC). Our app will be secured by Auth0, call the Node API to fetch dog data, and call the API to acquire Firebase tokens to authorize users to add and delete comments in realtime with Cloud Firestore. The app will use shared modules as well as implement lazy loading.

Angular Firebase app with Auth0 custom tokens

To implement the app, you will need the following:

  • Angular CLI
  • A free Auth0 account with a Client and an API configured
  • A free Firebase project with a service account

Let’s get started!

Angular CLI

Make sure you have Node.js with NPM installed on your local machine. Run the following command to install the Angular CLI globally:

$ npm install -g @angular/cli@latest

We will generate our Angular app and nearly all of its architecture using the CLI.

Auth0 Client and API

You’ll need an Auth0 account to manage authentication. You can sign up for a free account here.

Auth0 login screen

Next, set up an Auth0 client app and API so Auth0 can interface with the Angular app and Node API.

Set Up an Auth0 Client

  1. Go to your Auth0 Dashboard and click the Create a New Client button.
  2. Name your new app (something like Angular Firebase) 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.
  4. Enable the toggle for Use Auth0 instead of the IdP to do Single Sign On.
  5. At the bottom of the Settings section, click “Show Advanced Settings”. Choose the OAuth tab and verify that the JsonWebToken Signature Algorithm is set to “RS256”.
  6. 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 uses username/password database, Facebook, Google, and Twitter.

Note: 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 Auth0 API

  1. Go to APIs in your Auth0 dashboard and click on the “Create API” button. Enter a name for the API, such as Firebase Dogs API. Set the Identifier to your API endpoint URL. In this tutorial, our API identifier is http://localhost:1337/. 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. In the next steps, 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 back-end API.

Firebase Project with Service Account

Next you will need a free Firebase project.

Create a Firebase Project

  1. Go to the Firebase Console and sign in with your Google account.
  2. Click on Add Project.
  3. In the dialog that pops up, give your project a name (such as Angular Firebase Auth0). A project ID will be generated based on the name you chose. You can then select your country/region.
  4. Click the Create Project button.

Generate an Admin SDK Key

In order to mint custom Firebase tokens, you’ll need access to the Firebase Admin SDK. To obtain access, you must create a service account in your new Firebase project.

Click on the gear wheel icon next to your Project Overview in the Firebase console sidebar and select Project Settings from the menu that appears:

Firebase project setting

In the settings view, click the Service Accounts tab. The Firebase Admin SDK UI will appear, showing a configuration code snippet. Node.js is selected by default. This is the technology we want, and we will implement it in our Node API. Click on the Generate New Private Key button.

A dialog will appear warning you to store your private key confidentially. We will take care never to check this key into a public repository. Click on the Generate Key button to download the key as a .json file. We will add this file to our Node API shortly.

Node API

The completed Node.js API for this tutorial can be found at the firebase-auth0-nodeserver GitHub repo. Let’s learn how to build this API.

Node API File Structure

We’ll want to set up the following file structure:

firebase-auth0-nodeserver/
  |--firebase/
     |--.gitignore
     |--<your-firebase-admin-sdk-key>.json
  |--.gitignore
  |--config.js
  |--dogs.json
  |--package.json
  |--routes.js
  |--server.js  

You can generate the necessary folders and files with the command line like so:

$ mkdir firebase-auth0-nodeserver
$ cd firebase-auth0-nodeserver
$ mkdir firebase
$ touch firebase/.gitignore
$ touch .gitignore
$ touch config.js
$ touch dogs.json
$ touch package.json
$ touch routes.js
$ touch server.js

Firebase Admin SDK Key and Git Ignore

Now move the Firebase Admin SDK .json key file you downloaded earlier into the firebase folder. We will take care to make sure the folder is checked in, but its contents are never pushed to a repo using the firebase/.gitignore like so:

# firebase/.gitignore
*
*/
!.gitignore

This .gitignore configuration ensures that Git will ignore any files and folders inside the firebase directory except for the .gitignore file itself. This allows us to commit an (essentially) empty folder. Our .json Firebase Admin SDK key can live in this folder and we won’t have to worry about gitignoring it by filename.

Note: This is particularly useful if we have the project pulled down on multiple machines and have different keys (with different filenames) generated.

Next let’s add the code for the root directory’s .gitignore:

# .gitignore
config.js
node_modules

Dogs JSON Data

Next we’ll add the data for ten dog breeds. For brevity, you can simply copy and paste this data into your dogs.json file.

Dependencies

Let’s add our package.json file like so:

{
  "name": "firebase-auth0-nodeserver",
  "version": "0.1.0",
  "description": "Node.js server that authenticates with an Auth0 access token and returns a Firebase auth token.",
  "repository": "https://github.com/auth0-blog/firebase-auth0-nodeserver",
  "main": "server.js",
  "scripts": {
    "start": "node server"
  },
  "author": "Auth0",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {}
}

We’ll install the dependencies with the command line and latest versions will be saved automatically to the package.json file:

$ npm install --save body-parser cors express express-jwt jwks-rsa firebase-admin

We’ll need body-parser, cors, and express to serve our API endpoints. Authentication will rely on express-jwt and jwks-rsa, while Firebase token minting is implemented with the firebase-admin SDK (which we’ll have access to using the key we generated).

Configuration

In the config.js file, add the following code and replace the placeholder values with your own settings:

// config.js
module.exports = {
  AUTH0_DOMAIN: '<Auth0 Domain>', // e.g., you.auth0.com
  AUTH0_API_AUDIENCE: '<Auth0 API Audience>', // e.g., http://localhost:1337/
  FIREBASE_KEY: './firebase/<Firebase JSON>', // e.g., your-project-firebase-adminsdk-xxxxx-xxxxxxxxxx.json
  FIREBASE_DB: '<Firebase Database URL>' // e.g., https://your-project.firebaseio.com
};

Server

With our data, configuration, and dependencies in place, we can now implement our Node server. Open the server.js file and add:

// server.js
// Modules
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

// App
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());

// Set port
const port = process.env.PORT || '1337';
app.set('port', port);

// Routes
require('./routes')(app);

// Server
app.listen(port, () => console.log(`Server running on localhost:${port}`));

This will launch our Node server with Express at http://localhost:1337/.

Note: Notice that this is the API identifier we set up in Auth0.

API Routes

Next open the routes.js file. This is where we’ll define our API endpoints, secure them, and mint custom Firebase tokens. Add the following code:

// routes.js
// Dependencies
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
// Config
const config = require('./config');

module.exports = function(app) {
  // Auth0 athentication middleware
  const jwtCheck = jwt({
    secret: jwks.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`
    }),
    audience: config.AUTH0_API_AUDIENCE,
    issuer: `https://${config.AUTH0_DOMAIN}/`,
    algorithm: 'RS256'
  });

  // Initialize Firebase Admin with service account
  const serviceAccount = require(config.FIREBASE_KEY);
  firebaseAdmin.initializeApp({
    credential: firebaseAdmin.credential.cert(serviceAccount),
    databaseURL: config.FIREBASE_DB
  });

  // GET object containing Firebase custom token
  app.get('/auth/firebase', jwtCheck, (req, res) => {
    // Create UID from authenticated Auth0 user
    const uid = req.user.sub;
    // Mint token using Firebase Admin SDK
    firebaseAdmin.auth().createCustomToken(uid)
      .then(customToken =>
        // Response must be an object or Firebase errors
        res.json({firebaseToken: customToken})
      )
      .catch(err =>
        res.status(500).send({
          message: 'Something went wrong acquiring a Firebase token.',
          error: err
        })
      );
  });

  // Set up dogs JSON data for API
  const dogs = require('./dogs.json');
  const getDogsBasic = () => {
    const dogsBasicArr = dogs.map(dog => {
      return {
        rank: dog.rank,
        breed: dog.breed,
        image: dog.image
      }
    });
    return dogsBasicArr;
  }

  // GET dogs (public)
  app.get('/api/dogs', (req, res) => {
    res.send(getDogsBasic());
  });

  // GET dog details by rank (private)
  app.get('/api/dog/:rank', jwtCheck, (req, res) => {
    const rank = req.params.rank * 1;
    const thisDog = dogs.find(dog => dog.rank === rank);
    res.send(thisDog);
  });
};

At a high level, our routes file does the following:

  • Sets up authentication checking to ensure that only logged in users can access routes with jwtCheck middleware
  • Initializes the Firebase Admin SDK with the private key generated from the Firebase project service account
  • Provides a secure GET endpoint that returns a custom Firebase token
  • Provides a public GET* endpoint that returns a short version of the dogs data
  • Provides a secure GET* endpoint that returns a specific dog’s detailed data, requested by rank.

*Endpoints use variations of the same base dataset to simulate a more complex API.

You can read the code comments for more detail.

Serve the API

You can serve the Node API by running:

$ node server

The API will then be available at http://localhost:1337.

Note: If you try to access secure routes in the browser, you should receive a 401 Unauthorized error.

That’s it for our server! Keep the API running so that it will be accessible to the Angular app, which we’ll set up next.

Set Up Angular App

Now it’s time to create our Angular app and set up some additional dependencies.

Create New Angular App

You should have already installed the Angular CLI earlier. We can now use the CLI to generate our project and its architecture. To create a new app, choose a containing folder and then run the following command:

$ ng new angular-firebase --routing --skip-tests

The --routing flag generates an app with a routing module and --skip-tests generates the root component with no .spec.ts file.

Note: For brevity, we are not going to cover testing in this article. If you’d like to learn more about testing in Angular, check out the tutorial’s conclusion for more resources.

Install Front-end Dependencies

Now let’s install our front-end dependencies:

$ cd angular-firebase
$ npm install --save auth0-js@latest firebase@latest angularfire2@latest

We will need the auth0-js library to implement Auth0 authentication in our Angular app. We’ll also need the firebase JS SDK and the angularfire2 Angular Firebase library to implement our realtime comments with Firebase.

Add Bootstrap CSS

To simplify styling, we’ll add the Bootstrap CSS CDN link to the <head> of our index.html file like so:

<!-- src/index.html -->
...
<head>
  ...
  <title>Top 10 Dogs</title>
  ...
  <link
    rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
    crossorigin="anonymous">
</head>
...

Serve the Angular App

You can serve the Angular app with the following command:

$ ng serve

The app will run in the browser at http://localhost:4200.

Angular App Architecture

We’re going to use the Angular CLI to generate the complete architecture for our app up front. This way, we can make sure that our modules are functioning properly before we implement our logic and templates.

Our app is going to use a modular approach with lazy loading. The sample app in this tutorial is small, but we want to build it in a scalable, real-world manner.

Root Module

The root module has already been created when the Angular app was generated with the ng new command. The root module lives at src/app/app.module.ts. Any components we generate in our Angular app without another module’s subdirectory specified will be automatically imported and declared in our root module.

Let’s generate a component with the CLI now:

# create CallbackComponent:
$ ng g component callback --is --it --flat --no-spec

This command is composed of the following:

  • ng g component: generates a callback component file with:
  • --is inline styles
  • --it inline template
  • --flat no containing folder
  • --no-spec no .spec test file

We’ll use the callback component to handle redirection after the user logs into our application. It’s a very simple component.

Note: g is a shortcut for generate. We could also use c as a shortcut for component, making this command ng g c. However, this tutorial will not use shortcuts for the type of files generated, in the interest of clarity.

Core Module Architecture

Next we’ll create the CoreModule and its components and services. This is a shared module. From the root of your Angular project folder, run the following CLI commands. Make sure you run the ng g module core command first, like so:

# create Core module:
$ ng g module core
# create API service with no .spec file:
$ ng g service core/api --no-spec
# create HeaderComponent with inline styles, no .spec file, and export in module:
$ ng g component core/header --is --no-spec --export=true
# create LoadingComponent with inline styles, inline template, no folder, no .spec file, and export in module:
$ ng g component core/loading --is --it --flat --no-spec --export=true
# create ErrorComponent with inline styles, inline template, no folder, no .spec file, and export in module:
$ ng g component core/error --is --it --flat --no-spec --export=true
# create Dog type interface:
$ ng g interface core/dog
# create DogDetail type interface:
$ ng g interface core/dog-detail

Creating the module first ensures that components created in that module’s folder will then be imported and declared automatically in that parent module instead of the app’s root module.

Note: If you wish to use a shared module’s components in another module, you need to export the components as well as declare them. We can do this automatically with the CLI using the --export=true flag.

This is the basic architecture for the shared core services, components, and models that our app will need access to.

Auth Module Architecture

Next we’ll create our AuthModule. Execute the following CLI commands (again, making sure to generate the module first):

# create Auth module:
$ ng g module auth
# create AuthService with no .spec file:
$ ng g service auth/auth --no-spec
# create Auth route guard with no .spec file:
$ ng g guard auth/auth --no-spec

Our Auth module supplies the service and route guard we need to manage authentication, but does not have any components. This is also a shared module.

Dogs Module Architecture

Our app’s homepage will be provided by the DogsModule. This will be the list of ten most popular dogs in 2016 as ranked by the AKC. Use the following CLI commands to generate the structure for this lazy-loaded page module:

# create Dogs module:
$ ng g module dogs
# create DogsComponent with inline styles and no .spec file:
$ ng g component dogs/dogs --is --no-spec

Dog Module Architecture

Our app will also have detail pages for each dog listed in the Dogs component so that users can learn more about each breed. Use the following CLI commands to generate the structure for the lazy-loaded DogModule:

# create Dog module:
$ ng g module dog
# create DogComponent with inline styles and no .spec file:
$ ng g component dog/dog --is --no-spec

Comments Module Architecture

Finally, we need to implement the architecture necessary for our Firebase realtime comments. Use the following CLI commands to generate the structure for the CommentsModule:

# create Comments module:
$ ng g module comments
# create Comment model class:
$ ng g class comments/comment
# create CommentsComponent with no .spec file:
$ ng g component comments/comments --no-spec --export=true
# create CommentFormComponent with inline styles and no .spec file:
$ ng g component comments/comments/comment-form --is --no-spec

Environment Configuration

Let’s add our configuration information for Auth0 and Firebase to our Angular front end. Open the environment.ts file and add:

// src/environments/environment.ts
const FB_PROJECT_ID = '<FIREBASE_PROJECT_ID>';

export const environment = {
  production: false,
  auth: {
    clientId: '<AUTH0_CLIENT_ID>',
    clientDomain: '<AUTH0_DOMAIN>', // e.g., you.auth0.com
    audience: '<AUTH0_API_AUDIENCE>', // e.g., http://localhost:1337/
    redirect: 'http://localhost:4200/callback',
    scope: 'openid profile email'
  },
  firebase: {
    apiKey: '<FIREBASE_API_KEY>',
    authDomain: `${FB_PROJECT_ID}.firebaseapp.com`,
    databaseURL: `https://${FB_PROJECT_ID}.firebaseio.com`,
    projectId: FB_PROJECT_ID,
    storageBucket: `${FB_PROJECT_ID}.appspot.com`,
    messagingSenderId: '<FIREBASE_MESSAGING_SENDER_ID>'
  },
  apiRoot: '<API URL>' // e.g., http://localhost:1337/ (DO include trailing slash)
};

Replace placeholders in <angle brackets> with your appropriate Auth0, Firebase, and API information.

You can find your Auth0 configuration in your Auth0 Dashboard in the settings for the client and API you created for this tutorial.

You can find your Firebase configuration in the Firebase Console Project Overview after clicking the large icon labeled Add Firebase to your web app, as shown below:

Add Firebase to your web app

Add Loading Image

The last thing we’ll do before we begin implementing functionality in our Angular app is add a loading image. Create the following folder: src/assets/images.

Then save this loading SVG image into that folder:

Loading SVG

Implement Shared Modules

Let’s set up our modules. We’ll import the shared modules (CoreModule and AuthModule) in our root AppModule.

Core Module

First we’ll implement our CoreModule. Open the core.module.ts file and update to the following code:

// src/app/core/core.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { DatePipe } from '@angular/common';
import { HeaderComponent } from './header/header.component';
import { ApiService } from './api.service';
import { LoadingComponent } from './loading.component';
import { ErrorComponent } from './error.component';

@NgModule({
  imports: [
    CommonModule,
    RouterModule,
    HttpClientModule, // AuthModule is a sibling and can use this without us exporting it
    FormsModule
  ],
  declarations: [
    HeaderComponent,
    LoadingComponent,
    ErrorComponent
  ],
  exports: [
    FormsModule, // Export FormsModule so CommentsModule can use it
    HeaderComponent,
    LoadingComponent,
    ErrorComponent
  ]
})
export class CoreModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [
        Title,
        DatePipe,
        ApiService
      ]
    };
  }
}

Since this is a shared module, we’ll import the other modules, services, and components that we’ll need access to throughout our app.

Note: The CommonModule is imported in all modules that are not the root module.

In our imports array, we’ll add any modules that may be needed by services or components in the CoreModule, or that need to be available to other modules in our app. The CLI should have automatically added any generated components to the declarations array. The exports array should contain any modules or components that we want to make available to other modules.

Note that we have imported ModuleWithProviders from @angular/core. Using this module, we can create a forRoot() method that can be called on import in the root app.module.ts when CoreModule is imported. This way, we can ensure that any services we add to a providers array returned by the forRoot() method remain singletons in our application. In this manner, we can avoid unintentional multiple instances if other modules in our app also need to import the CoreModule.

Auth Module

Next let’s add some code to our AuthModule in the auth.module.ts file:

// src/app/auth/auth.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { AngularFireAuthModule } from 'angularfire2/auth';

@NgModule({
  imports: [
    CommonModule,
    AngularFireAuthModule
  ]
})
export class AuthModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: AuthModule,
      providers: [
        AuthService,
        AuthGuard
      ]
    };
  }
}

We’ll import ModuleWithProviders to implement a forRoot() method like we did with our CoreModule. Then we’ll import our AuthService and AuthGuard. We also need to import AngularFireAuthModule from angularfire2/auth so we can secure our Firebase connections in our AuthService. The service and guard should then be returned in the providers array in the forRoot() method.

Comments Module

Open the comments.module.ts file to implement the CommentsModule like so:

// src/app/comments/comments.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module';
import { environment } from './../../environments/environment';
import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { CommentsComponent } from './comments/comments.component';
import { CommentFormComponent } from './comments/comment-form/comment-form.component';

@NgModule({
  imports: [
    CommonModule,
    CoreModule, // Access FormsModule, Loading, and Error components
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule
  ],
  declarations: [
    CommentsComponent,
    CommentFormComponent
  ],
  exports: [
    CommentsComponent
  ]
})
export class CommentsModule { }

We’ll need to import the CoreModule so we can utilize its exported FormsModule, LoadingComponent, and ErrorComponent. We also need to access our configuration from the environment.ts file. Comments use Firebase’s Cloud Firestore database, so let’s import the AngularFireModule and AngularFirestoreModule as well as our two components: CommentsComponent and CommentFormComponent.

When we add AngularFireModule to the @NgModule’s imports array, we’ll call its initializeApp() method, passing in our Firebase configuration. Both of our components should already be in the declarations array, and the CommentsComponent should already be added to the exports array so that other components from other modules can use it.

Note: We don’t need to export CommentsFormComponent because it’s a child of CommentsComponent.

The CommentsModule does not provide any services, so there’s no need to implement a forRoot() method.

App Module

Now that our CoreModule, AuthModule, and CommentsModule have been implemented, we need to import them in our root module, the AppModule located in the app.module.ts file:

// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';
import { CommentsModule } from './comments/comments.module';
import { AppComponent } from './app.component';
import { CallbackComponent } from './callback.component';

@NgModule({
  declarations: [
    AppComponent,
    CallbackComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CoreModule.forRoot(),
    AuthModule.forRoot(),
    CommentsModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The AppComponent and CallbackComponent have already been added automatically by the CLI. When we add our CoreModule and AuthModule to the imports array, we’ll call the forRoot() method to ensure no extra instances are created for their services. The CommentsModule doesn’t provide any services, so this is not a concern for that module.

Implement Routing and Lazy Loaded Modules

We have two modules that require routing: the DogsModule for the main listing of dogs, and the DogModule, which contains the component showing a dog breed’s detail page.

App Routing

First let’s implement our app’s routing. Open the app-routing.module.ts file and add this code:

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CallbackComponent } from './callback.component';
import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
  {
    path: '',
    loadChildren: './dogs/dogs.module#DogsModule',
    pathMatch: 'full'
  },
  {
    path: 'dog',
    loadChildren: './dog/dog.module#DogModule',
    canActivate: [
      AuthGuard
    ]
  },
  {
    path: 'callback',
    component: CallbackComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

We’ll import our CallbackComponent and AuthGuard. The remaining routes will be string references to modules rather than imported components using the loadChildren property.

We will set the default '' path to load route children from the DogsModule, and the 'dog' path to load route children from the DogModule. The 'dog' path should also be protected by the AuthGuard, which we declare using the canActivate property. This can hold an array of route guards should we require more than one. Finally, the 'callback' route should simply point to the CallbackComponent.

Dogs Module

Let’s add some code to the dogs.module.ts file:

// src/app/dogs/dogs.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { CoreModule } from '../core/core.module';
import { CommentsModule } from '../comments/comments.module';
import { DogsComponent } from './dogs/dogs.component';

const DOGS_ROUTES: Routes = [
  {
    path: '',
    component: DogsComponent
  }
];

@NgModule({
  imports: [
    CommonModule,
    CoreModule,
    RouterModule.forChild(DOGS_ROUTES),
    CommentsModule
  ],
  declarations: [
    DogsComponent
  ]
})
export class DogsModule { }

We’ll import Routes and RouterModule in addition to our CoreModule and CommentsModule (comments will appear on the main dogs listing page).

This module has a child route, so we’ll create a constant that contains an array to hold our route object. The only child route we’ll need inherits the '' path from app-routing.module.ts, so its path should also be ''. It will load the DogsComponent. In our imports array, we’ll pass our DOGS_ROUTES constant to the RouterModule‘s forChild() method.

Dog Module

The DogModule works similarly to the DogsModule above. Open dog.module.ts and add the following:

// src/app/dog/dog.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { CoreModule } from '../core/core.module';
import { DogComponent } from './dog/dog.component';

const DOG_ROUTES: Routes = [
  {
    path: ':rank',
    component: DogComponent
  }
];

@NgModule({
  imports: [
    CommonModule,
    CoreModule,
    RouterModule.forChild(DOG_ROUTES)
  ],
  declarations: [
    DogComponent
  ]
})
export class DogModule { }

One difference between this module and the DogsModule is that our DOG_ROUTES has a path of :rank. This way, the route for any specific dog’s details is passed as a URL segment matching the dog’s rank in our list of top ten dog breeds, like so:

http://localhost:4200/dog/3

Another difference is that we will not import the CommentsModule. However, we could add comments to dog details in the future if we wished.

Our app’s architecture and routing are now complete! The app should successfully compile and display in the browser, with lazy loading functioning properly to load shared code and the code for the specific route requested.

We’re now ready to implement our application’s logic.

Loading and Error Components

The loading and error components are basic, core UI elements that can be used in many different places in our app. Let’s set them up now.

Loading Component

The LoadingComponent should simply show a loading image. (Recall that we already saved one when we set up the architecture of our app.) However, it should be capable of displaying the image large and centered, or small and inline.

Open the loading.component.ts file and add:

// src/app/core/loading.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-loading',
  template: `
    <div [ngClass]="{'inline': inline, 'text-center': !inline, 'py-2': !inline }">
      <img src="/assets/images/loading.svg">
    </div>
  `,
  styles: [`
    .inline {
      display: inline-block;
    }
    img {
      height: 80px;
      width: 80px;
    }
    .inline img {
      height: 24px;
      width: 24px;
    }
  `]
})
export class LoadingComponent {
  @Input() inline: boolean;
}

Using the @Input() decorator, we can pass information into the component from its parent, telling it whether we should display the component inline or not. We’ll use the NgClass directive ([ngClass]) in our template to conditionally add the appropriate styles for the display we want. Displaying this component in another template will look like this:

<!-- Large, full width, centered: -->
<app-loading></app-loading>
<!-- Inline: -->
<app-loading inline="true"></app-loading>

Error Component

Next let’s quickly implement our ErrorComponent. This component will display a simple error message if shown. Open the error.component.ts file and add:

// src/app/core/error.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-error',
  template: `
    <p class="alert alert-danger">
      <strong>Error:</strong> There was an error retrieving data.
    </p>
  `
})
export class ErrorComponent {
}

Authentication Logic

Now let’s implement the code necessary to get our AuthModule‘s features working. We’ll need the authentication service in order to build out the header in the CoreModule, so it makes sense to start here. We’ve already installed the necessary dependencies (Auth0 and FirebaseAuth), so let’s begin.

Authentication Service

Before we write any code, we’ll determine what the requirements are for this service. We need to:

  • Create a login() method that will allow users to authenticate using Auth0
  • If user was prompted to log in by attempting to access a protected route, make sure they can be redirected to that route after successful authentication
  • Get the user’s profile information and set up their session
  • Establish a way for the app to know whether the user is logged in or not
  • Request a Firebase custom token from the API with authorization from the Auth0 access token
  • If successful in acquiring a Firebase token, sign into Firebase using the returned token and establish a way for the app to know whether the user is logged into Firebase or not
  • Custom tokens minted by Firebase expire after an hour, so we should set up a way to automatically renew tokens that expire
  • Create a logout() method to clear session and sign out of Firebase.

Open the auth.service.ts file that we generated earlier.

For tutorial brevity, please check out the full code in the GitHub repo’s auth.service.ts file here.

There’s a lot going on, so let’s go through it step by step.

First, as always, we’ll import our dependencies. This includes our environment configuration we set up earlier to provide our Auth0, Firebase, and API settings, as well as auth0 and firebase libraries, AngularFireAuth, HttpClient to call the API to get a custom Firebase token, and the necessary RxJS imports.

You can refer to the code comments for descriptions of the private and public members of our AuthService class.

Next is our constructor function, where we’ll make Router, AngularFireAuth, and HttpClient available for use in our class.

The login() method looks like this:

login(redirect?: string) {
  // Set redirect after login
  const _redirect = redirect ? redirect : this.router.url;
  localStorage.setItem('auth_redirect', _redirect);
  // Auth0 authorize request
  this._auth0.authorize();
}

If a redirect URL segment is passed into the method, we’ll save it in local storage. If no redirect is passed, we’ll simply store the current URL. We’ll then use the _auth0 instance we created in our members and call Auth0’s authorize() method to go to the Auth0 login page so our user can authenticate.

The next three methods are handleLoginCallback(), getUserInfo(), and _setSession():

handleLoginCallback() {
  this.loading = true;
  // When Auth0 hash parsed, get profile
  this._auth0.parseHash((err, authResult) => {
    if (authResult && authResult.accessToken) {
      window.location.hash = '';
      // Store access token
      this.accessToken = authResult.accessToken;
      // Get user info: set up session, get Firebase token
      this.getUserInfo(authResult);
    } else if (err) {
      this.router.navigate(['/']);
      this.loading = false;
      console.error(`Error authenticating: ${err.error}`);
    }
  });
}

getUserInfo(authResult) {
  // Use access token to retrieve user's profile and set session
  this._auth0.client.userInfo(this.accessToken, (err, profile) => {
    if (profile) {
      this._setSession(authResult, profile);
    } else if (err) {
      console.warn(`Error retrieving profile: ${err.error}`);
    }
  });
}

private _setSession(authResult, profile) {
  // Set tokens and expiration in localStorage
  const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + Date.now());
  localStorage.setItem('expires_at', expiresAt);
  this.userProfile = profile;
  // Session set; set loggedIn and loading
  this.loggedIn = true;
  this.loading = false;
  // Get Firebase token
  this._getFirebaseToken();
  // Redirect to desired route
  this.router.navigateByUrl(localStorage.getItem('auth_redirect'));

These methods are fairly self-explanatory: they use Auth0 methods parseHash() and userInfo() to extract authentication results and get the user’s profile. We’ll also set our service’s properties to store necessary state (such as whether the user’s authentication state is loading and if they’re logged in or not), handle errors, save data to our service and local storage, and redirect to the appropriate route.

We are also going to use the authentication result’s access token to authorize an HTTP request to our API to get a Firebase token. This is done with the _getFirebaseToken() and _firebaseAuth() methods:

  private _getFirebaseToken() {
    // Prompt for login if no access token
    if (!this.accessToken) {
      this.login();
    }
    const getToken$ = () => {
      return this.http
        .get(`${environment.apiRoot}auth/firebase`, {
          headers: new HttpHeaders().set('Authorization', `Bearer ${this.accessToken}`)
        });
    };
    this.firebaseSub = getToken$().subscribe(
      res => this._firebaseAuth(res),
      err => console.error(`An error occurred fetching Firebase token: ${err.message}`)
    );
  }

  private _firebaseAuth(tokenObj) {
    this.afAuth.auth.signInWithCustomToken(tokenObj.firebaseToken)
      .then(res => {
        this.loggedInFirebase = true;
        // Schedule token renewal
        this.scheduleFirebaseRenewal();
        console.log('Successfully authenticated with Firebase!');
      })
      .catch(err => {
        const errorCode = err.code;
        const errorMessage = err.message;
        console.error(`${errorCode} Could not log into Firebase: ${errorMessage}`);
        this.loggedInFirebase = false;
      });
  }

We’ll create a getToken$ observable from the GET request to our API’s /auth/firebase endpoint and subscribe to it. If successful, we’ll pass the returned object with the custom Firebase token to the _firebaseAuth() method, which will authenticate with Firebase using Firebase’s signInWithCustomToken() method. This method returns a promise, and when the promise is resolved, we can tell our app that Firebase login was successful. We can also schedule Firebase token renewal (we’ll look at this shortly). We’ll handle any errors appropriately.

Our custom Firebase token will expire in 3600 seconds (1 hour). This is only half as long as our default Auth0 access token lifetime (which is 7200 seconds, or 2 hours). To avoid having our users lose access to Firebase unexpectedly in the middle of a session, we’ll set up automatic Firebase token renewal with two methods: scheduleFirebaseRenewal() and unscheduleFirebaseRenewal().

Note: You can also implement automatic session renewal with Auth0 in a similar manner using the checkSession() method. In addition, you could use checkSession() to restore an unexpired authentication session in the constructor if a user navigates away from the app and then returns later. We won’t cover that in this tutorial, but this is something you should try on your own!

scheduleFirebaseRenewal() {
  // If user isn't authenticated, check for Firebase subscription
  // and unsubscribe, then return (don't schedule renewal)
  if (!this.loggedInFirebase) {
    if (this.firebaseSub) {
      this.firebaseSub.unsubscribe();
    }
    return;
  }
  // Unsubscribe from previous expiration observable
  this.unscheduleFirebaseRenewal();
  // Create and subscribe to expiration observable
  // Custom Firebase tokens minted by Firebase
  // expire after 3600 seconds (1 hour)
  const expiresAt = new Date().getTime() + (3600 * 1000);
  const expiresIn$ = Observable.of(expiresAt)
    .pipe(
      mergeMap(
        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.refreshFirebaseSub = expiresIn$
    .subscribe(
      () => {
        console.log('Firebase token expired; fetching a new one');
        this._getFirebaseToken();
      }
    );
}

unscheduleFirebaseRenewal() {
  if (this.refreshFirebaseSub) {
    this.refreshFirebaseSub.unsubscribe();
  }
}

To schedule automatic token renewal, we’ll create a timer observable that counts down to the token’s expiration time. We can subscribe to the expiresIn$ observable and then call our _getFirebaseToken() method again to acquire a new token. The signInWithCustomToken() angularfire2 auth method returns a promise. When the promise resolves, scheduleFirebaseRenewal() is called, which in turn ensures that the token will continue to be renewed as long as the user is logged into our app.

We’ll also need to be able to unsubscribe from token renewal, so we’ll create a method for that as well.

Finally, the last two methods in our authentication service are logout() and tokenValid():

logout() {
  // Ensure all auth items removed
  localStorage.removeItem('expires_at');
  localStorage.removeItem('auth_redirect');
  this.accessToken = undefined;
  this.userProfile = undefined;
  this.loggedIn = false;
  // Sign out of Firebase
  this.loggedInFirebase = false;
  this.afAuth.auth.signOut();
  // Return to homepage
  this.router.navigate(['/']);
}

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

The logout() method removes all session information from local storage and from our service, signs out of Firebase Auth, and redirects the user back to the homepage (the only public route in our app).

The tokenValid accessor method checks whether the Auth0 access token is expired or not by comparing its expiration to the current datetime. This can be useful for determining if the user needs a new access token; we won’t cover that in this tutorial, but you may want to explore Auth0 session renewal further on your own.

That’s it for our AuthService!

Callback Component

Recall that we created a CallbackComponent in our root module. In addition, we set our environment‘s Auth0 redirect to the callback component’s route. That means that when the user logs in with Auth0, they will return to our app at the /callback route with the authentication hash appended to the URI.

We created our AuthService with methods to handle authentication and set sessions, but currently these methods aren’t being called from anywhere. The callback component is the appropriate place for this code to execute.

Open the callback.component.ts file and add:

// src/app/callback.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth/auth.service';

@Component({
  selector: 'app-callback',
  template: `
    <app-loading></app-loading>
  `
})
export class CallbackComponent implements OnInit {

  constructor(private auth: AuthService) { }

  ngOnInit() {
    this.auth.handleLoginCallback();
  }

}

All our callback component needs to do is show the LoadingComponent while the AuthService‘s handleAuth() method executes. The handleLoginCallback() method will parse the authentication hash, get the user’s profile info, set their session, and redirect to the appropriate route in the app.

Auth Guard

Now that we’ve implemented the authentication service, we have access to the properties and methods necessary to effectively use authentication state throughout our Angular application. Let’s use this logic to implement our AuthGuard for protecting routes.

Using the Angular CLI should have generated some helpful boilerplate code, and we only have to make a few minor changes to ensure that our guarded routes are only accessible to authenticated users.

Note: It’s important to note that route guards on their own do not confer sufficient security. You should always secure your API endpoints, as we have done in this tutorial, and never rely solely on the client side to authorize access to protected data.

Open the auth.guard.ts file and make the following changes:

// src/app/auth/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private auth: AuthService) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    if (this.auth.loggedIn) {
      return true;
    } else {
      // Send guarded route to redirect after logging in
      this.auth.login(state.url);
      return false;
    }
  }
}

We’ll import AuthService add a constructor() function to make the service available in our route guard. The canActivate() method should return true if conditions are met to grant access to a route, and false if not. In our case, the user should be able to access the guarded route if they are authenticated. The loggedIn property from our AuthService provides this information.

If the user does not have a valid token, we’ll prompt them to log in. We want them to be redirected back to the guarded route after they authenticate, so we’ll call the login() method and pass the guarded route (state.url) as the redirect parameter.

Note: Remember that we set up our entire app’s architecture and routing earlier. We already added AuthGuard to our dog details route, so it should be protected now that we’ve implemented the guard.

Core Logic

The last thing we’ll do in this section of our tutorial is build out the remaining components and services that belong to our CoreModule. We’ve already taken care of the LoadingComponent and ErrorComponent, so let’s move on to the header.

Header Component

The header will use methods and logic from our authentication service to show login and logout buttons as well as display the user’s name and picture if they’re authenticated. Open the header.component.ts file and add:

// src/app/core/header/header.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../../auth/auth.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styles: [`
    img {
      border-radius: 100px;
      width: 30px;
    }
    .loading { line-height: 31px; }
    .home-link { color: #212529; }
    .home-link:hover { text-decoration: none; }
  `]
})
export class HeaderComponent {

  constructor(public auth: AuthService) {}

}

We’ll add a few simple styles and import our AuthService to make its members publicly available to our header component’s template.

Next open the header.component.html file and add:

<!-- src/app/core/header/header.component.html -->
<nav class="nav justify-content-between mt-2 mx-2 mb-3">
  <div class="d-flex align-items-center">
    <strong class="mr-1"><a routerLink="/" class="home-link">Popular Dogs ❤</a></strong>
  </div>
  <div class="ml-3">
    <small *ngIf="auth.loading" class="loading">
      Logging in...
    </small>
    <ng-template [ngIf]="!auth.loading">
      <button
        *ngIf="!auth.loggedIn"
        class="btn btn-primary btn-sm"
        (click)="auth.login()">Log In</button>
      <span *ngIf="auth.loggedIn">
        <img [src]="auth.userProfile.picture">
        <small>{{ auth.userProfile.name }}</small>
        <button
          class="btn btn-danger btn-sm"
          (click)="auth.logout()">Log Out</button>
      </span>
    </ng-template>
  </div>
</nav>

The header now shows:

  • The name of our app (“Popular Dogs”) with a link to the / route
  • A login button if the user is not authenticated
  • A “Logging in…” message if the user is currently authenticating
  • The user’s picture, name, and a logout button if the user is authenticated

Now that we have our header component built, we need to display it in our app.

Open the app.component.html file and add:

<!-- src/app/app.component.html -->
<app-header></app-header>
<div class="container">
  <router-outlet></router-outlet>
</div>

The header component will now be displayed in our app with the current routed component showing beneath it. Check it out in the browser and try logging in!

Dog and DogDetail Models

Let’s implement our dog.ts and dog-detail.ts interfaces. These are models that specify types for the shape of values that we’ll use in our app. Using models ensures that our data has the structure that we expect.

We’ll start with the dog.ts interface:

// src/app/core/dog.ts
export interface Dog {
  breed: string;
  rank: number;
  image: string;
}

Next let’s implement the dog-detail.ts interface:

// src/app/core/dog-detail.ts
export interface DogDetail {
  breed: string;
  rank: number;
  description: string;
  personality: string;
  energy: string;
  group: string;
  image: string;
  link: string;
}

API Service

With our Node API and models in place, we’re ready to implement the service that will call our API in the Angular front end.

Open the api.service.ts file and add this code:

// src/app/core/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { environment } from './../../environments/environment';
import { AuthService } from './../auth/auth.service';
import { Observable } from 'rxjs/Observable';
import { catchError } from 'rxjs/operators';
import 'rxjs/add/observable/throw';
import { Dog } from './../core/dog';
import { DogDetail } from './../core/dog-detail';

@Injectable()
export class ApiService {
  private _API = `${environment.apiRoot}api`;

  constructor(
    private http: HttpClient,
    private auth: AuthService) { }

  getDogs$(): Observable<Dog[]> {
    return this.http
      .get(`${this._API}/dogs`)
      .pipe(
        catchError((err, caught) => this._onError(err, caught))
      );
  }

  getDogByRank$(rank: number): Observable<DogDetail> {
    return this.http
      .get(`${this._API}/dog/${rank}`, {
        headers: new HttpHeaders().set('Authorization', `Bearer ${this.auth.accessToken}`)
      })
      .pipe(
        catchError((err, caught) => this._onError(err, caught))
      );
  }

  private _onError(err, caught) {
    let errorMsg = 'Error: Unable to complete request.';
    if (err instanceof HttpErrorResponse) {
      errorMsg = err.message;
      if (err.status === 401 || errorMsg.indexOf('No JWT') > -1 || errorMsg.indexOf('Unauthorized') > -1) {
        this.auth.login();
      }
    }
    return Observable.throw(errorMsg);
  }

}

We’ll add the necessary imports to handle HTTP in Angular along with the environment configuration, AuthService, RxJS imports, and Dog and DogDetail models we just created. We’ll set up private members for the _API and to store the _accessToken, then make the HttpClient and AuthService available privately to our API service.

Our API methods will return observables that emit one value when the API is either called successfully or an error is thrown. The getDogs$() stream returns an observable with an array of objects that are Dog-shaped. The getDogByRank$(rank) stream requires a numeric rank to be passed in, and will then call the API to retrieve the requested Dog‘s data. This API call will send an Authorization header containing the authenticated user’s access token.

Finally, we’ll create an error handler that checks for errors and assesses if the user is not authenticated and prompts for login if so. The observable will then terminate with an error.

Note: We are using arrow functions to pass parameters to our handler functions for RxJS pipeable operators (such as catchError). This is done to preserve the scope of the this keyword (see the “No separate this” section of the MDN arrow functions documentation).

Next Steps

We’ve already accomplished a lot in the first part of our tutorial series. In the next part, we’ll finish our Popular Dogs application. In the meantime, here are some additional resources that you may want to check out:

Angular Testing Resources

If you’re interested in learning more about testing in Angular, which we did not cover in this tutorial, please check out some of the following resources:

Additional Resources

You can find more resources on Firebase, Auth0, and Angular here:

In the next installment of our Auth0 + Firebase + Angular tutorial, we’ll display data from our dogs API and learn how to set up and implement realtime comments with Firebase! Check out Authenticating Firebase and Angular with Auth0: Part 2 now.