How to Authenticate Firebase and Angular with Auth0: Part 1 - Custom Tokens & Lazy Loading

This series was updated to Angular, Angular CLI, and RxJS 6 in October 2018, as well as `@angular/fire` 5.

TL;DR: In this 2-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 FirebaseCloud 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.

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.

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.

Set Up an Auth0 Application

In the Settings for your new Auth0 application app, add http://localhost:4200/callback to the Allowed Callback URLs.

Enable the toggle for Use Auth0 instead of the IdP to do Single Sign On.

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".

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 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

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".

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.

Generate an Admin SDK Key

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:

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.

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.

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:

Serve the API

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.

Serve the Angular App

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.

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:

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):

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:

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:

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:

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 @angular/fire/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:

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:

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:

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.

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:

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.

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:

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

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.

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():

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:

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!

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():

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.

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.

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:

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.tsinterfaces. 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 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.

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: