Goals

In this codelab you will manage how users access data on the frontend of a web app by controlling user roles from a managed server-side environment. In practice, this could be any secure environment you control that the client cannot directly access, such as Google Cloud Functions or App Engine. In this example, your computer will play the role of a managed environment. You will be running the code locally. You will learn how to:

  1. Add custom claims (roles) to users with Firebase Authentication and the Firebase Admin SDK
  2. Verify claims on frontend and server-side environments to restrict access to data to authorized moderators.

Prerequisites

Before starting this codelab make sure you have installed:

In this section, you will download the project code and dependencies required to set up the FireFlicks web app.

Download the Code

Begin by cloning the sample project:

git clone https://github.com/firebase/fireflicks

Install Node packages

Before taking a look at the server-side code, let's set up the FireFlicks web app. This app lists movies and allows users to review them. Moderators are able to add new movies, which is where the server-side code comes in. You'll see that later on in the codelab.

Change to the directory of the web app, and then run npm install:

cd fireflicks/web_app
npm install

Installing the Node packages may take a while.

Inspect app

Take a look at what's inside this project.

ls

You'll see the following folders and files:

firebase.json
firestore.indexes.json
firestore.rules
index.html
manifest.webmanifest
node_modules
package-lock.json
package.json
src
tsconfig.json
webpack.config.js
yarn.lock

The firebase.json file contains information about different Firebase features the app is using. The firestore.rules dictates rules for accessing Cloud Firestore from the web app. You'll be deploying those rules shortly. The firestore-indexes.json contains indexes for sorting data in Cloud Firestore. The files package.json, package-lock.json, manifest.webmanifest, webpack.config.js, tsconfig.json, and yarn.lock all configure the appearance, behavior, and dependencies of the app. If you're a web developer, you're probably already familiar with these files. If not, don't worry about it! The directions will walk you through setup.

If you open index.html in Chrome, you'll notice the app doesn't look like much. In fact, it's a blank screen! There's still more setup to complete before the app is ready.

Create project

In the Firebase console click on Add Project and call it FireFlicks.

Click Create Project.

Enable Google Auth

To let users sign in on the web app you'll use Google auth, which needs to be enabled.

In the Firebase Console open the Authentication section > SIGN-IN METHOD tab (click here to go there) you need to enable the Google Sign-in Provider and click SAVE. This will allow users to sign in to the Web app with their Google accounts

Enable Anonymous Auth

You'll also be using anonymous auth, so this needs to be enabled as well.

In the Firebase Console open the Authentication section > SIGN IN METHOD tab (click here to go there) you need to enable the Anonymous Sign-in Provider and click SAVE. This allows users to sign in the Web app anonymously, allowing you to secure data to authenticated users without requiring a prompt to sign in right away.

Enable Cloud Firestore

The app uses Cloud Firestore to store data about movies. To enable Cloud Firestore on your Firebase project visit the Database section (click here to go there) and click the Create Database button on the Cloud Firestore window.

Then Click Enable on the popup about security rules. This enables the database in locked mode, which means the data is not yet accessible from the client.

Download the Service Account Key

Now you're going to download your project's ServiceAccountKey.json file from Firebase console and drag it to the fireflicks folder. To access the Service Account Key, go to Project Settings > Service Accounts (click here to go there) and select "GENERATE NEW KEY".

Move and Rename service account key file

Once the Service Account Key is downloaded, drag it into the fireflicks folder. The file has a long name, like fireflicks-4f400-...fidksf.json. Rename this file to exactly serviceAccountKey.json because this is what the project is looking for.

The Service Account Key is used to initialize the Admin SDK.

The Firebase Command Line Interface (CLI) allows you to serve the web app locally and deploy your web app.

To install or upgrade the CLI run the following npm command:

npm -g install firebase-tools

To verify that the CLI has been installed correctly, open a console and run:

firebase --version

Make sure the version of the Firebase CLI is above 3.5.0 so that it has all the latest features required for Cloud Functions. If not, run npm install -g firebase-tools to upgrade as shown above.

Authorize the Firebase CLI by running:

firebase login

Make sure you are in the web_app directory then set up the Firebase CLI to use your Firebase Project:

firebase use --add

Then select your Project ID and follow the instructions. When prompted, you can choose any Alias, such as codelab for instance.

Earlier, you enabled Cloud Firestore in locked mode. This means that if you were to run the web app now, no data would be accessible from the app even if the user is authenticated. Rules can be updated from the Firebase Console or the CLI. The project includes Cloud Firestore rules in a file called firestore.rules. Deploy these to your Firebase project using the command

firebase deploy --only firestore

Run the FireFlicks app

Once the rules deploy, the web app is configured to run. Run it locally with this command:

npm run dev

To view the app, open Chrome and navigate to http://localhost:8080/

You'll see something like this:

Log in to FireFlicks

The app doesn't display any movies yet. Next, you'll set up the server code, which will also populate the view with some movies.

But first, use the "Log In" button on the right of the toolbar to sign in using your Gmail account. In order to apply custom claims to your user, the user account must already exist in the Firebase project. Signing in now creates a new Firebase user with your email.

You will run a Node.js Express server which handles setting custom claims on user accounts. Server code is located in the backend/admin-service directory of the Git repo. Start by opening a new terminal window and installing the required dependencies:

cd backend/admin-service 
npm install

Next, build the server app.

npm run build

FireFlicks uses Cloud Firestore as its database. Run the following command to populate the database with some sample data. This creates two new collections named "movies" and "ratings" in your Firestore database.

npm run upload-data

Now we are ready to start the Express server. Following command starts a web server that listens on port 3000.

npm run serve

The server implementation uses the Firebase Admin SDK for Node.js (i.e. firebase-admin package). The Admin SDK gets initialized with the service account key you obtained earlier. Here's the relevant code fragment from the app.ts file:

admin-service/src/app.ts

import * as admin from 'firebase-admin';

const serviceAccount = require('../../../serviceAccountKey.json');
admin.initializeApp({
 credential: admin.credential.cert(serviceAccount),
});

Testing the endpoints

The web server exposes following endpoints:

Try calling the GET endpoint using curl or a web browser. You should get a 401 Unauthorized response.

curl -v http://localhost:3000/movies/test
> GET /movies/test HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.53.1
> Accept: */*
> 
< HTTP/1.1 401 Unauthorized
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 36
< ETag: W/"24-mZDp0pnuUM+7C5nI03U41Hl4kOA"
< Date: Mon, 11 Jun 2018 21:46:17 GMT
< Connection: keep-alive
< 
{"message":"ID token not specified"}

It seems the server is looking for an ID token value on the request. Try sending some random string as the ID token. You should still get a 401 response, but this time with a different error message.

curl -v -H "X-Firebase-ID-Token: random-value" http://localhost:3000/movies/test
> GET /movies/test HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.53.1
> Accept: */*
> X-Firebase-ID-Token: random-value
> 
< HTTP/1.1 401 Unauthorized
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 265
< ETag: W/"109-2Zi+hNzS1hAeN/IcOBoqIY1jbSc"
< Date: Mon, 11 Jun 2018 21:54:27 GMT
< Connection: keep-alive
< 
{"message":"Request not authorized","details":"Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token."}

Examine the code

As you can see our server implementation authorizes incoming requests based on the ID token provided. Each request must contain a valid Firebase ID token. This prevents unauthorized users from tampering with user roles and the movie database. Here's the relevant bit of server code from the app.ts file:

admin-service/src/app.ts

const idToken = req.header('X-Firebase-ID-Token');
if (!idToken) {
  return next(new HttpError('ID token not specified', 401));
}
try {
  await fireflicks.checkAuth(idToken);
} catch (err) {
  return next(new HttpError('Request not authorized', 401, err));
}

The checkAuth() method is implemented as follows. You can find it in the fireflicks.ts file. Notice how it uses the Admin SDK to verify ID tokens, and assert required claims.

admin-service/src/fireflicks.ts

export async function checkAuth(idToken: string): Promise<void> {
   const decoded = await admin.auth().verifyIdToken(idToken);
   if (decoded.moderator !== true) {
     throw new Error('User does not have moderator privileges');
   }
}

Note that this implementation also requires the caller of the service to be a moderator of the FireFlicks app. This is enforced by checking whether the "moderator" claim is present on the caller's decoded ID token. Therefore in order to test this service further, we need a user account with the moderator role. Open a new terminal window and navigate to admin-service

cd backend/admin-service

You should now have three terminal windows: one running the web app locally, one running the server locally, and one for the next command you're going to run. Run the following command with the email address you use to login to FireFlicks. This promotes your user account to moderator status by setting the required claim:

npm run promote -- your@email.address

Here's the relevant code that sets the moderator claim on the user account:

admin-service/src/fireflicks.ts

export async function grantModeratorRole(email: string): Promise<void> {
 const user = await admin.auth().getUserByEmail(email);
 if (user.customClaims && (user.customClaims as any).admin === true) {
   return;
 }
 return admin.auth().setCustomUserClaims(user.uid, {
   moderator: true,
 });
}

Methods like setCustomUserClaims() are only available in the Firebase Admin SDK, and require elevated privileges in a Firebase project to invoke. In this demo, you gain the necessary authorization from the service account credentials used to initialize the Admin SDK. You cannot execute such operations in a client-side environment due to the security implications.

Next you will invoke the Node.js Express back-end service through the FireFlicks web app. The web app authenticates users with Firebase Auth, and therefore can provision a valid Firebase ID token required to call the service. Thanks to the promote step earlier, the ID token issued to you is guaranteed to contain the required moderator claim.

Navigate to the web_app folder of the project. It may be easiest to open this app in an IDE that lets you view the whole project, such as VSCode. This allows you to easily navigate the different project components.

Navigate to src/components/ModAddMovie and open index.ts.

Notice that there is a base_url variable, which is http://localhost:3000/. This is the same port where you were testing requests earlier. If your server code was live, base_url would be equal to your server endpoint.

ModAddMovie/index.ts

export default class ModAddMovie extends Vue {
  base_url = "http://localhost:3000/";
  //...
}

FireFlicks creates a new movie in the database by running the onPublishMovie() function. Right now the body is just a list of TODOs. Replace the body of the function with the code below:

ModAddMovie/index.ts

  async onPublishMovie() {
    // if fields aren't correct, exit function
    if (!this.verifyFields()) {
      return;
    }
    const movie = await this.getMovie();
    const token = await this.fst.auth.currentUser.getIdToken();
    const result = await this.postData(`${this.base_url}movies`, movie, token);
    if (result) {
      alert("New Movie Created!");
      this.movie_title = "";
      this.movie_description = "";
      this.image_url = "";
    }
  }

The function first verifies that all required movie fields are filled out. It then creates a JSON object that contains the information about the movie. This data is passed to the postData function, along with the ID token of the current user. The string version of the url passed to postData is `${this.base_url}movies`, which is "http://localhost:3000/movies".

Below is the function postData(). Right now your version just has a couple of TODOs. Replace the body of the function with the code below:

ModAddMovie/index.ts

  async postData(url: string, data: {}, token: string) {
    return fetch(url, {
      body: JSON.stringify(data),
      credentials: "same-origin",
      headers: {
        "X-Firebase-ID-Token": token,
        "content-type": "application/json"
      },
      method: "POST",
      mode: "cors",
      redirect: "follow",
      referrer: "no-referrer",
    })
    .then(response => response.json());
  }
}

postData is the function that makes the post request to the server endpoint. In this case, that endpoint is the localhost. Thanks to hot reloading, the app will be updated in Chrome as soon as you save the file. Now the owner of that email (you, presumably) has the ability to add new movies. You may notice, however, that no Moderator tab is visible.

Surfacing the Moderator tab

Right click on the toolbar and click Inspect.

This opens developer tools so you can see what's going on under the hood of a web page. Under the myTopnav, you'll see a class, "dropdown-admin hidden". This is a flag that is set when the currently authenticated user doesn't have the moderator claim set.

Select the div with the class name "dropdown-admin hidden". Under "styles", you'll see that display is set to "none". The tag is simply hidden from view. This means that users with a little web development knowledge could inspect the page and find the source code. However, even if they found the server endpoint in the source code, they would get an unauthorized error, just as you saw earlier when you tried to make requests to it before adding your email as a moderator.

Refresh the Auth token

The reason the div is still hidden is because the auth token used to first sign you in did not include the custom claim. If the token is refreshed, it will have the Moderator claim. The easiest way to refresh the token is to sign out and back in. Go ahead and sign out and in. You should now see the Moderator tab.

Add a new movie

Great work! You've successfully configured the Moderator custom claim using the Admin SDK. Time to celebrate by adding a movie. Navigate to the Moderator tab and add a new movie. Looking for some inspiration for your movie? Use this:

Title: FireFlicks Codelab

Description: Follow a developer on their journey to create an app that can handle different types of users. You'll laugh, you'll cry, you'll learn!

Genres: Action, Comedy, Drama

ImageUrl: https://www.gstatic.com/mobilesdk/160503_mobilesdk/logo/2x/firebase_96dp.png

Click submit and a pop up appears letting you know the movie was added successfully.

Navigate to the home page and you'll see your new movie added to the list. Great job!

In this codelab, you learned how to add custom claims to Firebase Auth users through the Firebase Admin SDK. By setting custom claims on user accounts, you can assign different roles to the users of your app. You can then check if a given user has a certain role in our frontend and/or back-end code. Find the full solution in the fireflicks repository on the "complete" branch.

To learn more about Firebase Auth and the Admin SDK, visit the following resources:

Introduction to Firebase Auth

Getting Started with the Firebase Admin SDK

Introduction to the Admin Auth API

Implementing custom Auth claims with the Admin SDK