End user authentication for Cloud Run tutorial

This tutorial shows how to create a web application on Cloud Run with access restricted to logged-in users and data stored in PostgreSQL. Identity Platform integration handles end-user authentication and provides user ID tokens to authorize the service to query a Cloud SQL database. This service has a public client that lets external users log in and access a voting UI to cast votes.

For simplicity, this tutorial uses Google as a provider: users must sign in with a Google account. However, you can use other providers or authentication methods to sign in users.

This service minimizes security risks by using Secret Manager, to protect sensitive data used to connect to the Cloud SQL instance, and using a least-privilege service identity.

During an end-user authentication in this sample:

  1. The client requests a signed ID token for the user based on Identity Platform.
  2. The signed ID token is sent with requests to a server.
  3. The server verifies user's identity using that signed ID token and grants access to the Cloud SQL PostgreSQL database.

This tutorial does not show the use of the following authentication method:

  • IAM authentication, which uses IAM roles to automatically assert and verify identities. This is recommended for service-to-service authentication, not individual identities. To learn more about service-to-service authentication, see Securing Cloud Run services tutorial. Currently, IAM-based authentication and ID token methods can not be combined.

Objectives

  • Write, build, and deploy a service to Cloud Run that shows how to:

    • Authenticate end-users to access the Cloud Run service using Identity Platform.

    • Connect a Cloud Run service to a postgreSQL database using Secret Manager to handle sensitive data.

  • Create a least-privilege service identity for minimal access to Google Cloud resources.

Costs

This tutorial uses billable components of Google Cloud, including:

Use the Pricing Calculator to generate a cost estimate based on your projected usage.

You should be able to complete this project within the Free Trial credits.

New Cloud Platform users might be eligible for a free trial.

Before you begin

  1. Sign in to your Google Account.

    If you don't already have one, sign up for a new account.

  2. In the Google Cloud Console, on the project selector page, select or create a Google Cloud project.

    Go to the project selector page

  3. Make sure that billing is enabled for your Cloud project. Learn how to confirm that billing is enabled for your project.

  4. Enable the Cloud Run, Secret Manager, Cloud SQL, Container Registry, and Cloud Build APIs.

    Enable the APIs

  5. Install and initialize the Cloud SDK.

Setting up gcloud defaults

To configure gcloud with defaults for your Cloud Run service:

  1. Set your default project:

    gcloud config set project PROJECT-ID

    Replace PROJECT-ID with the name of the project you created for this tutorial.

  2. If you are using Cloud Run (fully managed), configure gcloud for your chosen region and specify managed as your Cloud Run platform:

    gcloud config set run/region REGION
    gcloud config set run/platform managed

    Replace REGION with the supported Cloud Run region of your choice.

Cloud Run locations

Cloud Run is regional, which means the infrastructure that runs your Cloud Run services is located in a specific region and is managed by Google to be redundantly available across all the zones within that region.

Meeting your latency, availability, or durability requirements are primary factors for selecting the region where your Cloud Run services are run. You can generally select the region nearest to your users but you should consider the location of the other Google Cloud products that are used by your Cloud Run service. Using Google Cloud products together across multiple locations can affect your service's latency as well as cost.

Cloud Run is available in the following regions:

Subject to Tier 1 pricing

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tokyo)
  • asia-northeast2 (Osaka)
  • europe-north1 (Finland)
  • europe-west1 (Belgium)
  • europe-west4 (Netherlands)
  • us-central1 (Iowa)
  • us-east1 (South Carolina)
  • us-east4 (Northern Virginia)
  • us-west1 (Oregon)

Subject to Tier 2 pricing

  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seoul, South Korea)
  • asia-southeast1 (Singapore)
  • asia-southeast2 (Jakarta)
  • asia-south1 (Mumbai, India)
  • australia-southeast1 (Sydney)
  • europe-west2 (London, UK)
  • europe-west3 (Frankfurt, Germany)
  • europe-west6 (Zurich, Switzerland)
  • northamerica-northeast1 (Montreal)
  • southamerica-east1 (Sao Paulo, Brazil)

If you already created a Cloud Run service, you can view the region in the Cloud Run dashboard in the Cloud Console.

Retrieving the code sample

To retrieve the code sample for use:

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

    Node.js

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

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

  2. Change to the directory that contains the Cloud Run sample code:

    Node.js

    cd nodejs-docs-samples/run/idp-sql/

Visualizing the architecture

Architectural Diagram
Diagram shows user log-in via a Google sign-in pop-up provided by IdP, then redirected back to Cloud Run with the user's identity.
  1. A user makes the first request to the service.

  2. The Cloud Run service provides the client with a user login form.

  3. The user logs in via the Google sign-in pop-up window that uses Identity Platform.

  4. The authentication flow redirects the user back to the Cloud Run service along with the user's identity.

  5. When the user casts a vote, the client mints an ID token and appends to server requests. The server verifies the ID token and grants write access to Cloud SQL.

Understanding the code

The sample is implemented as client and server, as described next.

Integrating with Identity Platform: client-side code

This sample uses Firebase SDKs to integrate with Identity Platform in order to sign-in and manage users. To connect to Identity Platform, the client-side javascript holds the reference to the project's credentials as a config object and imports the necessary Firebase JavaScript SDKs:

const config = {
  apiKey: 'API_KEY',
  authDomain: 'PROJECT_ID.firebaseapp.com',
};
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first-->
<script src="https://www.gstatic.com/firebasejs/7.18/firebase-app.js"></script>
<!-- Add Firebase Auth service-->
<script src="https://www.gstatic.com/firebasejs/7.18/firebase-auth.js"></script>

The Firebase JavaScript SDK handles the sign-in flow by prompting users to sign-in with their Google Accounts by opening a popup window and then redirecting back to the service.

function signIn() {
  const provider = new firebase.auth.GoogleAuthProvider();
  provider.addScope('https://www.googleapis.com/auth/userinfo.email');
  firebase
    .auth()
    .signInWithPopup(provider)
    .then(result => {
      // Returns the signed in user along with the provider's credential
      console.log(`${result.user.displayName} logged in.`);
      window.alert(`Welcome ${result.user.displayName}!`);
    })
    .catch(err => {
      console.log(`Error during sign in: ${err.message}`);
    });
}

When a user successfully signs in, Firebase methods mint ID tokens to uniquely identify and grant access for the user. The client communicates with the server by adding the Authorization header with the ID token.

async function vote(team) {
  if (firebase.auth().currentUser) {
    // Retrieve JWT to identify the user to the Identity Platform service.
    // Returns the current token if it has not expired. Otherwise, this will
    // refresh the token and return a new one.
    try {
      const token = await firebase.auth().currentUser.getIdToken();
      const response = await fetch('/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: `Bearer ${token}`,
        },
        body: 'team=' + team, // send application data (vote)
      });
      if (response.ok) {
        const text = await response.text();
        window.alert(text);
        window.location.reload();
      }
    } catch (err) {
      console.log('Error when voting: ' + err);
      window.alert('Somthing went wrong... Please try again!');
    }
  } else {
    window.alert('User not signed in.');
  }
}

Integrating with Identity Platform: server-side code

The server uses the Firebase Admin SDK to integrate with Identity Platform and verify the identity of the user from the user ID token that is sent from the client. If the provided ID token has the correct format, is not expired, and is properly signed, the method returns the decoded ID token needed for the extraction of the Identity Platform uid for that user.

const firebase = require('firebase-admin');
// Initialize Firebase Admin SDK
firebase.initializeApp();

// Extract and verify Id Token from header
const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (authHeader) {
    const token = authHeader.split(' ')[1];
    // If the provided ID token has the correct format, is not expired, and is
    // properly signed, the method returns the decoded ID token
    firebase
      .auth()
      .verifyIdToken(token)
      .then(decodedToken => {
        const uid = decodedToken.uid;
        req.uid = uid;
        next();
      })
      .catch(err => {
        req.logger.error(`Error with authentication: ${err}`);
        return res.sendStatus(403);
      });
  } else {
    return res.sendStatus(401);
  }
};

Connecting to Cloud SQL

The service connects to the Cloud SQL instance unix domain socket using the format: /cloudsql/CLOUD_SQL_CONNECTION_NAME.

/**
 * Connect to the Cloud SQL instance through UNIX Sockets
 *
 * @param {object} credConfig The Cloud SQL connection configuration from Secret Manager
 * @returns {object} Knex's PostgreSQL client
 */
const connectWithUnixSockets = async credConfig => {
  const dbSocketPath = process.env.DB_SOCKET_PATH || '/cloudsql';
  // Establish a connection to the database
  return Knex({
    client: 'pg',
    connection: {
      user: credConfig.DB_USER, // e.g. 'my-user'
      password: credConfig.DB_PASSWORD, // e.g. 'my-user-password'
      database: credConfig.DB_NAME, // e.g. 'my-database'
      host: `${dbSocketPath}/${credConfig.CLOUD_SQL_CONNECTION_NAME}`,
    },
    ...config,
  });
};

Handling sensitive configuration with Secret Manager

Secret Manager allows for centralized and secure storage of sensitive data such as Cloud SQL configuration. The service loads the Cloud SQL credentials from Secret Manager via the client library.

const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
let client;

async function getSecrets(secretName) {
  if (!client) client = new SecretManagerServiceClient();
  try {
    const [version] = await client.accessSecretVersion({name: secretName});
    return version.payload.data;
  } catch (err) {
    throw Error(`Error accessing Secret Manager: ${err}`);
  }
}

Shipping the service

Identity Platform setup

Identity Platform requires manual setup in the Cloud Console.

  1. Go to the Identity Platform Marketplace page in the Cloud Console.

  2. Click Enable Identity Platform.

    1. This step creates OAuth2 Client Id with name: Web client (auto created by Google Service)
  3. Configure Google as a provider:

    1. Go to the Identity Providers page in the Cloud Console.

    2. Click Add A Provider.

    3. Select Google from the list.

    4. Enter the Google Web Client ID and Web Secret (OAuth 2.0 Client ID) that was created by enabling Identity Platform in the previous step (name: Web client (auto created by Google Service)). You can also discover the name in the APIs & Services > Credentials page.

    5. Click Configure Screen.

      1. Select External for User type.

      2. Fill in the required fields (Support email).

    6. Under "Configure your application", click Setup Details. Copy the snippet into the sample's static/config.js to initialize the Identity Platform Client SDK.

    7. Click Save.

Deploying the service

Follow the steps below to complete infrascture provisioning and deployment or automate the process in Cloud Shell by clicking "Run on Google Cloud":

Run on Google Cloud

  1. Create a Cloud SQL instance with postgreSQL database using the console or CLI:

    gcloud sql instances create CLOUD_SQL_INSTANCE_NAME \
        --database-version=POSTGRES_12 \
        --region=CLOUD_SQL_REGION \
        --cpu=2 \
        --memory=7680MB \
        --root-password=DB_PASSWORD
  2. Add your Cloud SQL credential values to postgres-secrets.json:

    {
      "CLOUD_SQL_CONNECTION_NAME": "PROJECT_ID:REGION:INSTANCE",
      "DB_NAME": "postgres",
      "DB_USER": "postgres",
      "DB_PASSWORD": "PASSWORD_SECRET"
    }
    
  3. Create a versioned secret using the console or CLI:

    gcloud secrets create idp-sql-secret \
        --replication-policy="automatic" \
        --data-file=postgres-secrets.json
  4. Create a service account using the console or CLI:

    gcloud iam service-accounts create idp-sql-identity
  5. Add bindings for Secret Manager and Cloud SQL access using the console or CLI:

    1. Allow service account to access the created secret:

      gcloud secrets add-iam-policy-binding idp-sql-secret \
        --member serviceAccount:idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \
        --role roles/secretmanager.secretAccessor
    2. Allow service account to access Cloud SQL:

      gcloud projects add-iam-policy-binding PROJECT_ID \
        --member serviceAccount:idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \
        --role roles/cloudsql.client
  6. Build the container image using Cloud Build:

    gcloud builds submit --image gcr.io/PROJECT_ID/idp-sql
  7. Deploy the container image to Cloud Run using the console or CLI:

    gcloud run deploy idp-sql \
        --image gcr.io/PROJECT_ID/idp-sql \
        --allow-unauthenticated \
        --service-account idp-sql-identity@PROJECT_ID.iam.gserviceaccount.com \
        --add-cloudsql-instances PROJECT_ID:CLOUD_SQL_REGION:CLOUD_SQL_INSTANCE_NAME \
        --update-env-vars SECRETS=projects/PROJECT_ID/secrets/idp-sql-secret/versions/1

    Specifically using flags, --service-account, --add-cloudsql-instances, and --update-env-vars to specify the service identity, the Cloud SQL instance connection, and the full secret path name as an environment variable, respectively.

Finishing touches

Authorize the Cloud Run service URL as an allowed redirect after signing in the user:

  1. Edit the Google provider by clicking the pen icon in the Identity Providers page.

  2. Click Add Domain under Authorized Domains and enter the Cloud Run service URL.

    You can locate the service URL in the logs after the build or deployment or you can find it anytime using:

    gcloud run services describe vote --format 'value(status.url)'

Trying it out

To try out the complete service:

  1. Navigate your browser to the URL provided by the deployment step above.

  2. Click the Sign in with Google button and go through the authentication flow.

  3. Add your vote!

    It should look like this:

    Screenshot of the User Interface shows vote count for each team and a list of votes.

If you choose to continue developing these services, remember that they have restricted Identity and Access Management (IAM) access to the rest of Google Cloud and will need to be given additional IAM roles to access many other services.

Cleaning up

If you created a new project for this tutorial, delete the project. If you used an existing project and wish to keep it without the changes added in this tutorial, delete resources created for the tutorial.

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 Cloud Console, go to the Manage resources page.

    Go to the Manage resources page

  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.

Deleting tutorial resources

  1. Delete the Cloud Run service you deployed in this tutorial:

    gcloud run services delete SERVICE-NAME

    Where SERVICE-NAME is your chosen service name.

    You can also delete Cloud Run services from the Google Cloud Console.

  2. Remove the gcloud default region configuration you added during tutorial setup:

     gcloud config unset run/region
    
  3. Remove the project configuration:

     gcloud config unset project
    
  4. Delete other Google Cloud resources created in this tutorial:

What's next