Signing in users on App Engine


This tutorial shows you how to retrieve, verify, and store third-party credentials using Identity Platform, the App Engine standard environment, and Datastore.

This document walks you through a simple note-taking application called Firenotes that stores users' notes in their own personal notebooks. Notebooks are stored per user, and identified by each user's unique Identity Platform ID. The application has the following components:

  • The frontend configures the sign-in user interface and retrieves the Identity Platform ID. It also handles authentication state changes and lets users see their notes.

  • FirebaseUI is an open-source, drop-in solution that simplifies authentication and UI tasks. The SDK handles user login, linking multiple providers to one account, recovering passwords, and more. It implements authentication best practices for a smooth and secure sign-in experience.

  • The backend verifies the user's authentication state and returns user profile information as well as the user's notes.

The application stores user credentials in Datastore by using the NDB client library, but you can store the credentials in a database of your choice.

Firenotes is based on the Flask web application framework. The sample app uses Flask because of its simplicity and ease of use, but the concepts and technologies explored are applicable regardless of which framework you use.

Objectives

By completing this tutorial, you'll accomplish the following:

  • Configure the user interface with FirebaseUI for Identity Platform.
  • Obtain an Identity Platform ID token and verify it using server-side authentication.
  • Store user credentials and associated data in Datastore.
  • Query a database using the NDB client library.
  • Deploy an app to App Engine.

Costs

This tutorial uses billable components of Google Cloud, including:

  • Datastore
  • Identity Platform

Use the Pricing Calculator to generate a cost estimate based on your projected usage. New Google Cloud users might be eligible for a free trial.

Before you begin

  1. Install Git, Python 2.7, and virtualenv. For more information on setting up your Python development environment, such as installing the latest version of Python, refer to Setting Up a Python Development Environment for Google Cloud.
  2. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  3. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  4. Install the Google Cloud CLI.
  5. To initialize the gcloud CLI, run the following command:

    gcloud init
  6. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  7. Install the Google Cloud CLI.
  8. To initialize the gcloud CLI, run the following command:

    gcloud init

If you have already installed and initialized the SDK to a different project, set the gcloud project to the App Engine project ID you're using for Firenotes. See Managing Google Cloud SDK Configurations for specific commands to update a project with the gcloud tool.

Cloning the sample app

To download the sample to your local machine:

  1. Clone the sample application repository to your local machine:

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

  2. Navigate to the directory that contains the sample code:

    cd python-docs-samples/appengine/standard/firebase/firenotes
    

Adding the user interface

To configure FirebaseUI for Identity Platform and enable identity providers:

  1. Add Identity Platform to your app by following these steps:

    1. Go to the Google Cloud console.
      Go to the Google Cloud console
    2. Select the Google Cloud project you want to use:
      • If you have an existing project, select it on the Select organization drop-down list at the top of the page.
      • If you don't have an existing Google Cloud project, create a new project in the Google Cloud console.
    3. Go to the Identity Platform Marketplace page in the Google Cloud console.
      Go to the Identity Platform Marketplace page
    4. On the Identity Platform Marketplace page, click Enable Customer Identity.
    5. Go to the Customer Identity Users page in the Google Cloud console.
      Go to the Users page
    6. On the top right, click application setup details.
    7. Copy the application setup details into your Web Application.

      // Obtain the following from the "Add Firebase to your web app" dialogue
      // Initialize Firebase
      var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        projectId: "<PROJECT_ID>",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<MESSAGING_SENDER_ID>"
      };
  2. Edit the backend/app.yaml file and enter your Google Cloud project ID in the environment variables:

    # Copyright 2021 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #      http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    runtime: python27
    api_version: 1
    threadsafe: true
    service: backend
    
    handlers:
    - url: /.*
      script: main.app
    
    env_variables:
      GAE_USE_SOCKETS_HTTPLIB : 'true'
    
  3. In the frontend/main.js file, configure the FirebaseUI login widget by selecting which providers you want to offer your users.

    // Firebase log-in widget
    function configureFirebaseLoginWidget() {
      var uiConfig = {
        'signInSuccessUrl': '/',
        'signInOptions': [
          // Leave the lines as is for the providers you want to offer your users.
          firebase.auth.GoogleAuthProvider.PROVIDER_ID,
          firebase.auth.FacebookAuthProvider.PROVIDER_ID,
          firebase.auth.TwitterAuthProvider.PROVIDER_ID,
          firebase.auth.GithubAuthProvider.PROVIDER_ID,
          firebase.auth.EmailAuthProvider.PROVIDER_ID
        ],
        // Terms of service url
        'tosUrl': '<your-tos-url>',
      };
    
      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
    }
  4. In the Google Cloud console, enable the providers you chose to keep:

    1. Go to the Customer Identity Providers page in the Google Cloud console.
      Go to the Providers page
    2. Click Add A Provider.
    3. On the Select a provider drop-down list, select the providers you want to use.
    4. Next to Enabled, click the button to enable the provider.
      • For third-party identity providers, enter the provider ID and secret from the provider's developer site. The Firebase docs give specific instructions in the "Before you begin" sections of the Facebook, Twitter, and GitHub guides.
      • For SAML and OIDC integrations, refer to the configuration at your IdP.
  5. Add your domain to the list of authorized domains in Identity Platform:

    1. Go to the Customer Identity Settings page in the Google Cloud console.
      Go to the Settings page
    2. Under Authorized Domains, click Add Domain.
    3. Enter the domain of your app in the following format:

      [PROJECT_ID].appspot.com
      

      Don't include http:// before the domain name.

Installing dependencies

  1. Navigate to the backend directory and complete the application setup:

    cd backend/
    
  2. Install the dependencies into a lib directory in your project:

    pip install -t lib -r requirements.txt
    
  3. In appengine_config.py, the vendor.add() method registers the libraries in the lib directory.

Running the application locally

To run the application locally, use the App Engine local development server:

  1. Add the following URL as the backendHostURL in main.js:

    http://localhost:8081

  2. Navigate to the root directory of the application. Then, start the development server:

    dev_appserver.py frontend/app.yaml backend/app.yaml
    
  3. Visit http://localhost:8080/ in a web browser.

Authenticating users on the server

Now that you have set up a project and initialized an application for development, you can walk through the code to understand how to retrieve and verify Identity Platform ID tokens on the server.

Getting an ID token from Identity Platform

The first step in server-side authentication is retrieving an access token to verify. Authentication requests are handled with the onAuthStateChanged() listener from Identity Platform:

firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    $('#logged-out').hide();
    var name = user.displayName;

    /* If the provider gives a display name, use the name for the
    personal welcome message. Otherwise, use the user's email. */
    var welcomeName = name ? name : user.email;

    user.getIdToken().then(function(idToken) {
      userIdToken = idToken;

      /* Now that the user is authenicated, fetch the notes. */
      fetchNotes();

      $('#user').text(welcomeName);
      $('#logged-in').show();

    });

  } else {
    $('#logged-in').hide();
    $('#logged-out').show();

  }
});

When a user is signed in, the Identity Platform getToken() method in the callback returns a Identity Platform ID token in the form of a JSON Web Token (JWT).

Verifying tokens on the server

After a user signs in, the frontend service fetches any existing notes in the user's notebook through an AJAX GET request. This requires authorization to access the user's data, so the JWT is sent in the Authorization header of the request using the Bearer schema:

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  })

Before the client can access server data, your server must verify the token is signed by Identity Platform. You can verify this token using the Google Authentication Library for Python. Use the authentication library's verify_firebase_token function to verify the bearer token and extract the claims:

id_token = request.headers["Authorization"].split(" ").pop()
claims = google.oauth2.id_token.verify_firebase_token(
    id_token, HTTP_REQUEST, audience=os.environ.get("GOOGLE_CLOUD_PROJECT")
)
if not claims:
    return "Unauthorized", 401

Each identity provider sends a different set of claims, but each has at least a sub claim with a unique user ID and a claim that provides some profile information, such as name or email, that you can use to personalize the user experience on your app.

Managing user data in Datastore

After authenticating a user, you need to store their data for it to persist after a signed-in session has ended. The following sections explain how to store a note as a Datastore entity and segregate entities by user ID.

Creating entities to store user data

You can create an entity in Datastore by declaring an NDB model class with certain properties such as integers or strings. Datastore indexes entities by kind; in the case of Firenotes, the kind of each entity is Note. For querying purposes, each Note is stored with a key name, which is the user ID obtained from the sub claim in the previous section.

The following code demonstrates how to set properties of an entity, both with the constructor method for the model class when the entity is created and through assignment of individual properties after creation:

data = request.get_json()

# Populates note properties according to the model,
# with the user ID as the key name.
note = Note(parent=ndb.Key(Note, claims["sub"]), message=data["message"])

# Some providers do not provide one of these so either can be used.
note.friendly_id = claims.get("name", claims.get("email", "Unknown"))

To write the newly created Note to Datastore, call the put() method on the note object.

Retrieving user data

To retrieve user data associated with a particular user ID, use the NDB query() method to search the database for notes in the same entity group. Entities in the same group, or ancestor path, share a common key name, which in this case is the user ID.

def query_database(user_id):
    """Fetches all notes associated with user_id.

    Notes are ordered them by date created, with most recent note added
    first.
    """
    ancestor_key = ndb.Key(Note, user_id)
    query = Note.query(ancestor=ancestor_key).order(-Note.created)
    notes = query.fetch()

    note_messages = []

    for note in notes:
        note_messages.append(
            {
                "friendly_id": note.friendly_id,
                "message": note.message,
                "created": note.created,
            }
        )

    return note_messages

You can then fetch the query data and display the notes in the client:

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  }).then(function(data){
    $('#notes-container').empty();
    // Iterate over user data to display user's notes from database.
    data.forEach(function(note){
      $('#notes-container').append($('<p>').text(note.message));
    });
  });
}

Deploying your app

You have successfully integrated Identity Platform with your App Engine application. To see your application running in a live production environment:

  1. Change the backend host URL in main.js to https://backend-dot-[PROJECT_ID].appspot.com. Replace [PROJECT_ID] with your project ID.
  2. Deploy the application using the Google Cloud SDK command-line interface:

    gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml
    
  3. View the application live at https://[PROJECT_ID].appspot.com.

Clean up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, delete your App Engine project:

Deleting the project

The easiest way to eliminate billing is to delete the project that you created for the tutorial.

To delete the project:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

What's next

  • Explore reference architectures, diagrams, and best practices about Google Cloud. Take a look at our Cloud Architecture Center.