Ultimate Guide to User Authorization with Identity Platform
Tags: googlecloud, go, node, nextjs
Published: 2023-07-29 12:49:47

Recently, I added user authentication and role-based authorization (a.k.a RBAC) to one of my projects so that certain users could be granted **the** **admin role** and access our internal tools. 

As my servers run on Google Cloud Platform, I decided to go with **Google Identity Platform** (GIP), which is essentially the same service as **Firebase Authentication with Identity Platform** (think Firebase Authentication on steroids). 

Instead of a proper documentation or coherent guide covering this commonly sought-after flow, Google handed me a mess of concepts and overlapping libraries. 

So, I decided to save you some time by providing you the desired (ultimate) guide covering the full user auth/authz flow. 

&nbsp;

## Agenda

* [Plan](#plan)

* [Choices Matter](#choices-matter)
    - [Code](#code)

* [Set Up](#set-up)

* [Sign in with Google](#sign-in-with-google)
    * [Add Provider](#add-provider-1)
    * [Add Client App](#add-client-app-1)
        * [React / Next Client](#react-next-client-1)
        * [Vanilla JS Client](#vanilla-js-client-1)

* [Sign in with Email / Password](#sign-in-with-email-password)
    * [Add provider](#add-provider-2)
    * [Add Client App](#add-client-app-2)
        * [React / Next Client](#react-next-client-2)
        * [Vanilla JS Client](#vanilla-js-client-2)

* [Assign Roles](#assign-roles)
    * [Add Blocking Functions](#add-blocking-functions)
    * [Set Triggers](#set-triggers)
    * [Optional: Restrict Registration](#optional-restrict-registration)

* [Test Client Flow](#test-client-flow)

* [Protect Data](#protect-data)
    * [Control Access in Firebase](#control-access-in-firebase)
    * [Control Access using Middlewares](#control-access-using-middlewares)
        * [Go Middlewares](#go-middlewares)
        * [Node Middlewares](#node-middlewares)

* [Wrap Up](#wrap-up)

&nbsp;

## Plan

![Full Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8nolcuijm2c2zvwpzu0.png)

&nbsp;

I’m gonna show you the implementation of the following flow:
1. User signs up (or in) via the web client
2. Google Identity Platform (GIP) verifies the request and triggers blocking functions
3. Blocking functions assign the role (admin or user) based on the email address
4. If the user is an admin, they’ll be redirected to the Admin page
5. If an unauthorized user comes straight to the Admin page →
6. redirect them Home
7. A request is made against the backend route
8. The route triggers the Auth middleware
9. The Auth middleware verifies the user against GIP
10. If the user exists, the Authz (authorization) middleware is called
11. If the user’s role is sufficient to continue, the handler is triggered
12. Handler responds

&nbsp;

## Choices Matter

As I have no clue what tech stack you use, I decided to make several implementations. You’ll be presented with different choices that will affect what you read and better fit your exact case. 

### Code

Regardless of your choices, all code is available in [this repo](https://github.com/MichalMoravik/google-identity-guide/tree/main).

You can choose from **React** (Next 13) or **Vanilla JavaScript** for frontend and **Go** or **Node** for backend middlewares.

If you experience any issues while following this guide, slide to my DMs on [x.com](https://twitter.com/MoravikMichal) (Twitter) or open a GitHub issue, I’ll help.

&nbsp;

## Set Up

First, enable Identity Platform, and add your first provider. 

> ❔ **Provider**
> A provider is a service, often third-party, that your users can choose to sign up or sign in with. Examples: Google, Email / Password, Apple, …

1. Go to your [GCP console](https://console.cloud.google.com/), and open or add a project
2. [Enable the Identity Platform](https://console.cloud.google.com/marketplace/details/google-cloud-platform/customer-identity)
3. Click the **Providers** tab
4. Click the **ADD A PROVIDER** button

For this guide, I've created two implementations: Google and Email/Password, to illustrate the flow for the most common cases. If your chosen provider is different, you can still follow along with the Google implementation, as the flow is similar for other providers.

&nbsp;

> ❕ **Choose the Provider**
> [Sign in with Google](#sign-in-with-google)
> [Sign in with Email / Password](#sign-in-with-email-password)

&nbsp;

## Sign in with Google

### Add Provider <a name="add-provider-1"></a>

1. From the dropdown, select **Google** as the provider
2. Under **Web SDK configuration** section, you’ll have to supply the ID and secret
    1. Either click the **APIs and services page** link or search (by pressing **/**) for **APIs and services**
    2. Under **Credentials** tab, you’ll see **OAuth 2.0 Client IDs** section. Click on **Web client**
    3. Find and paste **Client ID** and **secret** on the right side
3. Skip **Allowed client IDs**
4. Click **CONFIGURE SCREEN** button. This will open **OAuth consent screen**
    1. Under **User Type** choose **External**, click **CREATE**
    2. Fill in **User support email**
    3. Under **Developer contact information**, add your email address
    4. You can ignore everything else (incl. the app name). At any point you can come back and edit things
    5. Click **SAVE AND CONTINUE**
    6. You don’t have to continue; only step #1 was required. When you’re ready for the public release later on, you come back and fill out everything properly. Now, return to the provider setup
5. Add the domains you'll use when developing your client app to the **Authorized Domains** section (located on the right side of the page). By default, **localhost** is already present, but if you use **127.0.0.1** during development, make sure to add it. Additionally, if you already know your public (prod) domain, add it now
6. Click **SAVE**. You’ll now see **the Google provider** **enabled**

&nbsp;

### Add Client App <a name="add-client-app-1"></a>

Let’s add a simple client so users can sign up and in. Now, this is where the overlapping of Google Cloud Platform and Firebase begins. 

![Intersection GCP and Firebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjz7i146jx864tmfktqh.png)

**Google Identity Platform (GIP)** shares the same backend as **Firebase Authentication with Identity Platform**. 

Since these two are identical products, Google decided *not* to duplicate themselves in certain places and ask you to use their **Firebase client SDK**. I've prepared two different implementations for you, one in **React** (using Next 13) and one in **Vanilla JS**. Pick one below.

&nbsp;

> ❕ **Pick Client**
> [React / Next Client](#react-next-client-1)
> [Vanilla JS Client](#vanilla-js-client-1)

&nbsp;

#### React / Next Client <a name="react-next-client-1"></a>

I'll use React with Next.js 13 as an example here, but you can use the code without Next and achieve the same functionality. In the end, it is just a simple React JavaScript code.

1. Run `npx create-next-app@latest`
2. Install the Firebase client SDK `npm i firebase`
3. Add a file **firebaseConfig.js** under **app** folder
4. Go back to the Identity Platform’s dashboard and under **Providers** tab click **APPLICATION SETUP DETAILS** Copy **apiKey** and **authDomain**

5. Add the following to your **firebaseConfig.js**

    ```jsx
    import { initializeApp } from 'firebase/app';
    import { getAuth } from 'firebase/auth';
    
    const firebaseConfig = {
      apiKey: '<your apiKey>',
      authDomain: '<your authDomain>',
    }
    
    const app = initializeApp(firebaseConfig);
    export const auth = getAuth(app);
    ```

6. Change your **page.js** to include this function

    > 🔨 If you get **Module not found: Can’t resolve ‘encoding’ …** error, run `npm i -D encoding`

    ```jsx
    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from './firebaseConfig';
    import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
    import { useRouter } from 'next/navigation'; // <--- Next-specific Router. Use your own React Router if you don't use Next
    
    export default function Home() {
      const provider = new GoogleAuthProvider();
      const router = useRouter();
    
    	const signInWithGoogle = async () => {
        try {
          await signInWithPopup(auth, provider);
    
          // Print token to test the middlewares later on via HTTP client
          // console.log(await auth.currentUser.getIdToken(true));
    
          const { claims } = await auth.currentUser.getIdTokenResult(true);
          if (claims.role === 'admin') router.push('/admin');
        } catch (error) {
          console.log(error);
        }
      }
    
      return (
        <div>
          <button onClick={signInWithGoogle}>Sign in with Google</button>
        </div>
      )
    }
    ```

    1. Press the button and you’ll be redirected to the Google popup, where you can choose your account. The user is then **created and signed in simultaneously**

    2. Completing the sign-up populates `auth.currentUser`. You can then obtain the current user’s (JWT) token and decode it to retrieve the user’s data, including **claims**

        > ❔ **Claims**
        > Claims are simply data about the user. They are relevant to the authorization flow because, during backend authentication, you can set custom claims on the user that can later be retrieved to determine their level of access.

    3. The **role** in the code snippet above is a custom claim that you'll set later on in this guide. If the user's **role claim** is set to **admin**, they will be redirected to the React page dedicated to admins

7. Create a new directory called **admin** and a new file named **page.js** inside it. Then, add the following code

    ```jsx
    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from '../firebaseConfig';
    import { useRouter } from 'next/navigation';
    import { useEffect } from 'react';
    
    export default function Admin() {
      const router = useRouter();
    
      useEffect(() => {
        auth.onAuthStateChanged(async (user) => {
          if (!user) router.push('/');
    
          const { claims } = await user.getIdTokenResult(true);
          if (claims.role !== 'admin') router.push('/');
        });
      }, []);
    
      return (
        <div>
          <h1>Admin</h1>
        </div>
      )
    }
    ```

    The moment Firebase Auth triggers the callback function, you simply check if the user exists and if the **role** claim is set to **admin**. If not, you want to redirect such a visitor to the Home page, not displaying the admin view. This is mainly used to redirect visitors coming straight to this page without authenticating first.

Now, jump to the section where you’ll discover how to assign roles to your users.

&nbsp;

> ❕ **[Jump to Assign Roles Section](#assign-roles)**

&nbsp;

#### Vanilla JS Client <a name="vanilla-js-client-1"></a>

Let’s add a simple client using vanilla JS embedded as a script tag inside HTML

1. Go back to the Identity Platform’s dashboard and under **Providers** tab click **APPLICATION SETUP DETAILS**. Copy **apiKey** and **authDomain**
2. Create a new HTML file called **auth-google.html** and insert the following scripts just before the closing body tag

    ```html
    <!-- ... -->
    <body>		
    		<!-- ... your other tags should be above ... -->
        <button onclick="signInWithGoogle()">Sign in with Google</button>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: "<your apiKey>",
                authDomain: "<your authDomain>",
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function signInWithGoogle() {
                const provider = new firebase.auth.GoogleAuthProvider();
    
                firebase.auth().signInWithPopup(provider)
                // Print token to test the middlewares later on via HTTP client
                /* .then(() => {
                    firebase.auth().currentUser.getIdToken(true)
                    .then(token => console.log(token))
                }) */
                .then(() =>
                    firebase.auth().currentUser.getIdTokenResult(true)
                    .then(result => {
                        if (result.claims.role === 'admin') {
                            window.location.href = 'auth-google-admin.html';
                        }
                    })
                    .catch(error => console.log(error))
                )
                .catch(error => console.log(error));
            }
        </script>
    </body>
    <!-- ... -->
    ```

    1. Press the button and you’ll be redirected to the Google popup, where you can choose your account. The user is then **created and signed in simultaneously**
    2. Completing the sign-up populates `auth.currentUser`. You can then obtain the current user’s (JWT) token and decode it to retrieve the user’s data (`result` in the code snippet above), including **claims** 

        > ❔ **Claims**
        > Claims are just data about the user. They’re relevant to the authorization flow as, while authenticating the user in the backend, you can set **custom** claims on the user that can later be retrieved to determine the level of access. 

    3. The **role** in the code snippet above is a custom claim that you'll set later on in this guide. If the user's **role claim** is set to **admin**, they will be redirected to the page dedicated to admins

4. In the same directory, create a new file called **auth-google-admin.html**, and add the following code

    ```html
    <!-- ... -->
    <body>
        <h1>Admin</h1>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: "<your apiKey>",
                authDomain: "<your authDomain>",
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function checkRole() {
                firebase.auth().onAuthStateChanged(user => {
                    if (!user) {
                        window.location.href = 'auth-google.html';
                    } else {
                        // Print token to test the middlewares later on via HTTP client
                        /* firebase.auth().currentUser.getIdToken(true)
                        .then(token => console.log(token))
                        .catch(error => console.log(error)); */
    
                        user.getIdTokenResult(true)
                        .then(result => {
                            if (result.claims.role !== 'admin') {
                                window.location.href = 'auth-google.html';
                            }
                        })
                        .catch(error => console.log(error));
                    }
                });
            }
            window.onload = checkRole;
        </script>
    </body>
    <!-- ... -->
    ```

    The moment Firebase Auth triggers the callback function, you simply check if the user exists and if the **role** claim is set to **admin**. If not, you want to redirect such a visitor to the **auth-google.html** page, not displaying the admin view. This is mainly used to redirect visitors coming straight to this page without authenticating first.

Now, jump to the section where you’ll discover how to assign roles to your users.

&nbsp;

> ❕ **[Jump to Assign Roles Section](#assign-roles)**

&nbsp;

## Sign in with Email / Password

### Add Provider <a name="add-provider-2"></a>

1. From the dropdown, select and enable **Email / Password** as the provider
2. **Uncheck** "Allow passwordless login" as we want to enable regular password login
3. Ignore the templates as you can tweak their content anytime later on
4. Add the domains you'll use when developing your client app to the **Authorized Domains** section (located on the right side of the page). By default, **localhost** is already present, but if you use **127.0.0.1** during development, make sure to add it. Additionally, if you already know your public (prod) domain, add it now
5. Click **SAVE**. You’ll now see the **Email / Password** **provider** **enabled**

&nbsp;

### Add Client App <a name="add-client-app-2"></a>

Let’s add a simple client so users can sign up and in. Now, this is where the overlapping of Google Cloud Platform and Firebase begins. 

![Intersection GCP and Firebase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmmtdlg03zno7plm8o4v.png)

**Google Identity Platform (GIP)** shares the same backend as **Firebase Authentication with Identity Platform**. 

Since these two are identical products, Google decided *not* to duplicate themselves in certain places and ask you to use their **Firebase client SDK**. I've prepared two different implementations for you, one in **React** (using Next 13) and one in **Vanilla JS**. Pick one below.

&nbsp;

> ❕ **Pick the Client**
> [React / Next Client](#react-next-client-2)
> [Vanilla JS Client](#vanilla-js-client-2)

&nbsp;

#### React / Next Client <a name="react-next-client-2"></a>

I'll use React with Next.js 13 as an example here, but you can use the code without Next and achieve the same functionality. In the end, it is just a simple React JavaScript code.

1. Run `npx create-next-app@latest`
2. Install the Firebase client SDK `npm i firebase`
3. Add a file **firebaseConfig.js** under **app** folder
4. Go back to the Identity Platform’s dashboard and under **Providers** tab click **APPLICATION SETUP DETAILS**. Copy **apiKey** and **authDomain**

5. Add the following to your **firebaseConfig.js**

    ```jsx
    import { initializeApp } from 'firebase/app';
    import { getAuth } from 'firebase/auth';
    
    const firebaseConfig = {
      apiKey: '<your apiKey>',
      authDomain: '<your authDomain>',
    }
    
    const app = initializeApp(firebaseConfig);
    export const auth = getAuth(app);
    ```

6. Change your **page.js** to include this simple function

    > 🔨 If you get **Module not found: Can’t resolve ‘encoding’ …** error, run `npm i -D encoding`

    ```jsx
    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { useState } from 'react';
    import { auth } from './firebaseConfig';
    import {
      createUserWithEmailAndPassword,
      signInWithEmailAndPassword,
      sendEmailVerification,
      signOut
    } from 'firebase/auth';
    import { useRouter } from 'next/navigation'; // <--- Next-specific Router. Use your own React Router if you don't use Next
    
    export default function Home() {
      const router = useRouter();
    
      // ... States omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-react-next/auth-email/app/page.js)
    
      const registerWithEmail = async (email, password) => {
        try {
          const cred = await createUserWithEmailAndPassword(auth, email, password);
    
    			// Change the redirect URL to env var if you want
          await sendEmailVerification(cred.user, { url: 'http://localhost:3000' });
          setMsg('Please verify your email before signing in');
    
          await signOut(auth);
        } catch (err) {
          console.log('Unexpected error: ', err);
        }
      }
    
      const signInWithEmail = async (email, password) => {
        try {
          await signInWithEmailAndPassword(auth, email, password);
    
          // Print token to test the middlewares later on via HTTP client
          // console.log(await auth.currentUser.getIdToken(true));
    
          const { claims } = await auth.currentUser.getIdTokenResult(true);
    
          if (!claims.email_verified) {
    				// Change the redirect URL to env var if you want
            await sendEmailVerification(auth.currentUser, { url: 'http://localhost:3000' });
            setMsg('Please verify your email before signing in. We have sent you another verification email');
            await signOut(auth);
            return;
          }
    
          console.log('Signed in as: ', claims.email);
    
          if (claims.role === 'admin') router.push('/admin');
        } catch (err) {
          console.log('Unexpected error: ', err);
        }
      }
    
      return (
        <div>
            { //... HTML tags omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-react-next/auth-email/app/page.js) }
        </div>
      )
    }
    ```

    **registerWithEmail**
    
    1. When `createUserWithEmailAndPassword()` is executed, the user is **created and signed in simultaneously**
    2. Right after that, `sendEmailVerification()` sends an email to verify their address. You can alter its content by editing the provider settings

        > ❔ **Sending Verification Email before First Sign-in**
        > You might ask, “Can I send the verification email before the user is first signed in?” The answer is no. What I present above is the only way to achieve this result. [I elaborated on this problem in this StackOverflow question](https://stackoverflow.com/questions/76703779/blocking-functions-are-broken-and-unable-to-prevent-sign-in-before-verifying-ema).

    3. Often, you want the user to verify their email address before signing them in. Since they are already signed in (due to registration in step #1), you must sign them out

    > 💡 TIP: In the future, you might want to consider adding reCAPTCHA

    **signInWithEmail**

    1. Press the button and your user will be logged in. Completing the sign-up populates `auth.currentUser`
    2. You can then obtain the current user’s (JWT) token and decode it using `getIdTokenResult()` to retrieve the user’s data, including **claims**

        > ❔ **Claims**
        > Claims are simply data about the user. They are relevant to the authorization flow because, during backend authentication, you can set custom claims on the user that can later be retrieved to determine their level of access.

    3. In case the user has not verified their address, you send them the email again and let them know they have to verify it first

        > 📕 **Note**
        > Anyone can modify your client code, so checking if the address has been verified doesn’t provide any security at this level; the client implementation has solely the UX purpose. You’re going to protect your data in the latter section (**Protect Data**).

    4. Lastly, you check if the user’s **role** is set to **admin**. This **role** is a **custom** claim that you’ll set later on in this guide. If the user’s **role** is **admin**, they’ll be redirected to the React page dedicated to admins

7. Create a new directory called **admin** and a new file named **page.js** inside it. Then, add the following code

    ```jsx
    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from '../firebaseConfig';
    import { useRouter } from 'next/navigation';
    import { useEffect } from 'react';
    
    export default function Admin() {
      const router = useRouter();
    
      useEffect(() => {
        auth.onAuthStateChanged(async (user) => {
          if (!user) router.push('/');
    
          const { claims } = await user.getIdTokenResult(true);
          if (claims.role !== 'admin') router.push('/');
        });
      }, []);
    
      return (
        <div>
          <h1>Admin</h1>
        </div>
      )
    }
    ```

    The moment Firebase Auth triggers the callback function, you simply check if the user exists and if the **role** claim is set to **admin**. If not, you want to redirect such a visitor to the Home page, not displaying the admin view. This is mainly used to redirect visitors coming straight to this page without authenticating first.

Now, jump to the section where you’ll discover how to assign roles to your users.

&nbsp;

> ❕ **[Jump to Assign Roles Section](#assign-roles)**

&nbsp;

#### Vanilla JS Client <a name="vanilla-js-client-2"></a>

Let’s add a simple client using vanilla JS embedded as a script tag inside HTML

1. Go back to the Identity Platform’s dashboard and under **Providers** tab click **APPLICATION SETUP DETAILS**. Copy **apiKey** and **authDomain**

2. Create a new HTML file called **auth-email.html** and insert the following scripts just before the closing body tag

    ```html
    <!-- ... -->
    <body>		
    		<!-- ... HTML tags omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-js/auth-email/auth-email.html) ... -->
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
          var config = {
            apiKey: "<your apiKey>",
            authDomain: "<your authDomain>",
          };
          firebase.initializeApp(config);
        </script>
        <script>
            // ... Listeners omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-js/auth-email/auth-email.html)
    
            function registerWithEmail(email, password) {
                firebase.auth().createUserWithEmailAndPassword(email, password)
                .then(cred => {
    								// Change the redirect URL to env var if you want
                    return cred.user.sendEmailVerification({
                        url: 'http://127.0.0.1:8080/auth-email.html'
                    })
                })
                .then(() => {
                    const msgTag = document.querySelector('#msg');
                    msgTag.innerHTML = `Please verify your email before signing in`;
    
                    return firebase.auth().signOut()
                })
                .catch(err => console.log('Unexpected error: ', err));
            }
    
            function signInWithEmail(email, password) {
                firebase.auth().signInWithEmailAndPassword(email, password)
                // Print token to test the middlewares later on via HTTP client
                /*.then(() => {
                    firebase.auth().currentUser.getIdToken(true)
                    .then(token => console.log(token))
                })*/
                .then(() => firebase.auth().currentUser.getIdTokenResult(true))
                .then(result => {
                    if (!result.claims.email_verified) {
                        return firebase.auth().currentUser.sendEmailVerification({
    												// Change the redirect URL to env var if you want
                            url: 'http://127.0.0.1:8080/auth-email.html'
                        })
                        .then(() => {
                            const msgTag = document.querySelector('#msg');
                            msgTag.innerHTML = `Please verify your email before
                            signing in. We have sent you another verification email`;
    
                            return firebase.auth().signOut()
                        })
                    }
    
                    console.log('Signed in as: ', result.claims.email);
    
                    if (result.claims.role === 'admin') {
                        window.location.href = 'auth-email-admin.html';
                    }
                })
                .catch(err => console.log('Unexpected error: ', err));
            }
        </script>
    </body>
    <!-- ... -->
    ```

    **registerWithEmail**
    
    1. When `createUserWithEmailAndPassword()` is executed, the user is **created and signed in simultaneously**
    2. Right after that, `sendEmailVerification()` sends an email to verify their address. You can alter its content by editing the provider settings

        > ❔ **Sending Verification Email before First Sign-in**
        > You might ask, “Can I send the verification email before the user is first signed in?” The answer is no. What I present above is the only way to achieve this result. [I elaborated on this problem in this StackOverflow question](https://stackoverflow.com/questions/76703779/blocking-functions-are-broken-and-unable-to-prevent-sign-in-before-verifying-ema).

    3. Often, you want the user to verify their email address before signing them in. Since they are already signed in (due to registration in step #1), you must sign them out

    > 💡 TIP: In the future, you might want to consider adding reCAPTCHA

    **signInWithEmail**

    1. Press the button and your user will be logged in. Completing the sign-up populates `auth.currentUser`
    2. You can then obtain the current user’s (JWT) token and decode it using `getIdTokenResult()` to retrieve the user’s data, including **claims**

        > ❔ **Claims**
        > Claims are simply data about the user. They are relevant to the authorization flow because, during backend authentication, you can set custom claims on the user that can later be retrieved to determine their level of access.

    3. In case the user has not verified their address, you send them the email again and let them know they have to verify it first

        > 📕 **Note**
        > Anyone can modify your client code, so checking if the address has been verified doesn’t provide any security at this level; the client implementation has solely the UX purpose. You’re going to protect your data in the latter section (**Protect Data**).

    4. Lastly, you check if the user’s **role** is set to **admin**. This **role** is a **custom** claim that you’ll set later on in this guide. If the user’s **role** is **admin**, they’ll be redirected to the React page dedicated to admins

3. In the same directory, create a new file called **auth-email-admin.html**, and add the following code

    ```html
    <!-- ... -->
    <body>
        <h1>Admin</h1>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: '<your apiKey>',
                authDomain: '<your authDomain>',
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function checkRole() {
                firebase.auth().onAuthStateChanged(user => {
                    if (!user) {
                        window.location.href = 'auth-email.html';
                    } else {
                        // Print token to test the middlewares later on via HTTP client
                        /* firebase.auth().currentUser.getIdToken(true)
                        .then(token => console.log(token))
                        .catch(error => console.log(error)); */
    
                        user.getIdTokenResult(true)
                        .then(result => {
                            if (result.claims.role !== 'admin') {
                                window.location.href = 'auth-email.html';
                            }
                        })
                        .catch(error => console.log(error));
                    }
                });
            }
            window.onload = checkRole;
        </script>
    </body>
    <!-- ... -->
    ```

    The moment Firebase Auth triggers the callback function, you simply check if the user exists and if the **role** claim is set to **admin**. If not, you want to redirect such a visitor to the **auth-email.html** page, not displaying the admin view. This is mainly used to redirect visitors coming straight to this page without authenticating first.

In the next section, you’ll discover how to assign roles to your users.

&nbsp;

## Assign Roles

![Blocking Functions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikd8ddfnoav3s5de1oap.png)

The diagram above illustrates the parts you've covered so far.

Your focus now is on assigning a custom **role** claim to users so you could control who has access to what data and services.

Obviously, the assignment has to be done in a secure environment (backend) to ensure that only you are able to manage the logic.

The way to implement this is by using **triggers**. When a certain event happens in GIP, a serverless function gets triggered and executes the logic of assigning the claim.

Originally, there used to be only one trigger useful in this situation - **[onCreate](https://firebase.google.com/docs/functions/auth-events#trigger_a_function_on_user_creation)**. This is still available (only v1) for the basic Firebase Authentication (legacy). However, the feature is limiting as it is triggered only *after* the new user is created. The main implication is that you cannot execute custom logic *before* a visitor signs up or signs in (e.g. you’re unable to restrict certain visitors from creating an account).

Due to the limitations, Google released **Blocking Functions**

&nbsp;

### Add Blocking Functions

Later on, Google added two new triggers - **beforeCreate** and **beforeSignIn**. These are used in combination with serverless Cloud Functions that can execute any custom logic **before** the user is created or signed in. They call them **[Blocking Functions](https://cloud.google.com/identity-platform/docs/blocking-functions)** because they can block visitors from creating an account or signing in based on conditions you specify within the functions’ body.

Google lets you choose from two distinct libraries to implement the identical function behaviour. One can be found in [the GIP docs](https://cloud.google.com/identity-platform/docs/blocking-functions), while the other one is in [the Firebase docs](https://firebase.google.com/docs/auth/extend-with-blocking-functions?gen=2nd). The only difference is that the Firebase library recently introduced a Python version (currently in preview) alongside its Node counterpart, whereas the GIP library currently supports only the Node implementation. I chose the one recommended by [the GIP docs](https://cloud.google.com/identity-platform/docs/blocking-functions).

I don’t use the inline editor in GCP for many reasons (no git, tracking, …), so the following are the steps I took to develop and deploy the **beforeSignIn** function from my machine.

**Implement**

1. Run `npm init`
2. Run `npm i gcip-cloud-functions`
3. I prefer ES modules so I added `"type": "module"` to my **package.json** configuration

4. Add a new file `index.js` with the following code

    ```js
    import gcipCloudFunctions from 'gcip-cloud-functions';
    import { beforeSignInHandler } from './handlers.js';
    
    const adminEmails = process.env.ADMIN_EMAILS.split(',') || [];
    const authClient = new gcipCloudFunctions.Auth();
    
    export const beforeSignIn = authClient.functions()
      .beforeSignInHandler((user, context) => beforeSignInHandler(user, context, adminEmails));
    ```

    You can specify the admin email addresses using the environment variable, which will be passed to the `beforeSignInHandler` function.

5. Add a new file `handlers.js` with the following code
    
    ```js
    export const beforeSignInHandler = (user, context, adminEmails) => {
      const role = adminEmails.includes(user.email) ? 'admin' : 'user';
    
      console.log(`Signing in user: ${user.email} with role: ${role}`);
      return {
        customClaims: { role },
      }
    };
    ```

    You can modify the user data by simply returning an object from the handler function. In the code above, we add a new custom **role** claim that can be set to either **admin** or **user**.

    I chose to use **beforeSignIn** trigger instead of the **beforeCreate** trigger to assign the **role** claim, as I want to have the ability to change the user’s access with each sign-in. GCP’s free tier allows me to ignore the resources here, but consider your own use case.

> 🔨 **[See Full Code in GitHub Repo](https://github.com/MichalMoravik/google-identity-guide/tree/main/blocking-functions)**
> There are more files, mostly related to unit tests. The babel package and **.babelrc** file are included to enable running Jest unit tests using ES modules.

**Deploy**

For deploying the function, I recommend using **[the gcloud CLI](https://cloud.google.com/cli)** and executing commands from your terminal or CI tool. 

1. Run `gcloud config set project <your project id>` to set the right project

2. Below is the command I use to deploy my function. Alter the flags for your case, especially the admin emails in the env variable (separate them using comma)

    ```sh
gcloud functions deploy beforeSignIn --runtime nodejs20 --trigger-http --allow-unauthenticated --region=europe-central2 --set-env-vars ADMIN_EMAILS='m@gmail.com'
    ```

For convenience, I suggest adding the command to **scripts** in the **package.json** file.

&nbsp;

### Set Triggers

After you deploy the function, you must hook this function up with the GIP **beforeSignIn** trigger.

1. Go to GCP Console (UI) → Identity Platform → Settings → Triggers
2. Hook up the function you just deployed to **beforeSignIn** and **SAVE**

&nbsp;

### Optional: Restrict Registration

While I was writing the guide, I realized the function’s logic is so short that it doesn’t even reflect the useful blocking ability of the blocking functions. So I decided to showcase the blocking in action by adding this bonus **whitelisting** feature, allowing only specific emails to register.

Now, you can choose anything as the whitelisting condition. Google supplies **user** and **context** arguments to your function that include handy data (e.g. user’s IP address, provider, email, …). The code below shows **whitelisting** **based on the email address**.

1. Create a new handler
    
    ```js
    import gcipCloudFunctions from 'gcip-cloud-functions';
    
    export const restrictRegistration = (user, context, whitelistEmails) => {
      console.log('New request to register user:', user.email);
      console.log('User Object:', user);
      console.log('Context Object:', context);
    
      if (!whitelistEmails.includes(user.email)) {
        console.log(`Unauthorized email "${user.email}"`);
        throw new gcipCloudFunctions.https.HttpsError(
          'permission-denied',
          `Unauthorized email "${user.email}"`
        );
      }
    
      console.log(`User with email: ${user.email} is authorized to register`);
    };
    ```

2. In **index.js**, import the handler and export the function
    
    ```js
    const whitelistEmails = process.env.WHITELIST_EMAILS.split(',') || [];
    
    export const beforeCreate = authClient.functions()
      .beforeCreateHandler((user, context) => restrictRegistration(user, context, whitelistEmails));
    ```

3. Change WHITELIST_EMAILS env var and deploy the function as **beforeCreate**
    
    ```sh
    gcloud functions deploy beforeCreate --runtime nodejs20 --trigger-http --allow-unauthenticated --region=europe-central2 --set-env-vars WHITELIST_EMAILS='m@gmail.com,k@gmail.com'
    ```

4. Set the **beforeCreate** trigger in Identity Platform’s settings

5. Handle the **permission-denied** error in the client

    ```js
    // or try-catch
    .catch(err => {
        if (err.code !== 'auth/internal-error') {
            console.log('Unexpected error: ', err);
        } else {
            const regStatus = err.message.match(/"status":"(.*?)"/);
            const status = regStatus ? regStatus[1] : '';
    
            if (status === 'PERMISSION_DENIED') {
    						// handle the returned error (e.g. display message)
            } else {
    		        console.log('Unhandled blocking error: ', err);
            }
        }
    });
    ```

&nbsp;

## Test Client Flow

![Client using Blocking Functions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y50mcf1d40x0nszxn6r8.png)

The diagram illustrates the current state of your implementation.

At this point, when signing in from your client using the admin email address, you’ll get redirected to the **Admin** page. Signing in using a non-admin email address will **not** trigger the redirection. 

> 📕 Note
> Anyone can modify your client code, so the client-side redirection doesn’t provide any security; the client implementation serves solely for UX purposes. You’ll protect your data in the next section (**Protect Data**).

If you experience different behaviour, debug the client and inspect the Cloud Function logs in GCP, or slide to my DMs on [x.com](https://twitter.com/MoravikMichal) (Twitter).

&nbsp;

## Protect Data

The diagram you saw in the previous section contains a greyed-out part. That is the part you'll focus on next, with the goal of protecting your data against unauthorized users.

Once your user signs in on the client, the (JWT) token included in the response gets stored locally in their browser. The token has to be sent whenever you make a request to the backend to verify its validity and extract the user data.

To make your life easier, whenever you use Firebase services such as Firestore, Realtime DB, or Storage, the token will be automatically sent with every request. If you use anything else, you'll have to include the token with the request.

The following sections will cover both cases, but feel free to jump straight to [middlewares](#control-access-using-middlewares) if you don’t plan to use any Firebase products.

&nbsp;

### Control Access in Firebase

In this case, when the sent token arrives, it gets automatically decoded by Firebase without you having to do anything. Then, the request proceeds to [Firebase Rules](https://firebase.google.com/docs/rules) where you specify conditions that have to be met in order for the user to access the data. 

```js
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function verifiedAdmin() {
    	return request.auth != null && request.auth.token.email_verified && request.auth.token.role == "admin";
    }
  
    match /admin/{document=**} {
    	// only allow to read the admin content if claim "role" is set "admin"
      allow read: if verifiedAdmin();
      allow write: if false;
    }
    match /{document=**} {
      allow read: if false;
      allow write: if false;
    }
  }
}
```

The Firebase Rules example above showcases how you could make the data inside the **admin** collection only accessible for reading if the user is a verified admin (see the `verifiedAdmin()` function). In the rules, the custom claims are accessible under `request.auth.token`.

The condition of `email_verified` to be true is only needed for [**untrusted providers**](https://cloud.google.com/identity-platform/docs/concepts-manage-users#verified_email_addresses), but I suggest you keep the condition as a precaution.

&nbsp;

### Control Access using Middlewares

In this case, you’ll protect route handlers in your REST API using authentication (**Auth**) and authorization (**AuthZ**) middlewares. See the steps #7 to #12 in the diagram below.

![Middlewares](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekobwk9euij45k9wsxfh.png)

First, I’ll first walk you through the implementation of the middlewares, and then, at the end, I’ll show you how you can test their functionality using a real (JWT) token. 

I’ve created two logically identical implementations, one in Go (v1.20), and the other in Node (v20). Yes, the versions are very fresh, you’re welcome. 

The full code (including unit tests, docker files, etc.) for both implementations is available in [the repo](https://github.com/MichalMoravik/google-identity-guide/tree/main).

In this guide, I’ll show you only the relevant code parts, but at the end, I’ll explain how to run and deploy such services.

&nbsp;

> ❕ **Choose your Backend**
> [Go Middlewares](#go-middlewares)
> [Node Middlewares](#node-middlewares)

&nbsp;

#### Go Middlewares

**Set Up**

To make my life easier, I added [Fiber](https://github.com/gofiber/fiber), a popular lightweight framework. Regardless of which package you use, the process and most of the code will remain unchanged.

Next, you’ll need to get the Firebase Admin SDK - `go get firebase.google.com/go`.

> ❔ **Firebase Admin SDK**
> You’ll use this SDK anytime you want your **backend** to communicate with Google Identity Platform. Again, in this case, Google decided to reuse the already developed Firebase library rather than making a new one specifically for Identity Platform.

**Implement**

1. Let the SDK know what project you are using by setting up **Firebase App**. Then initialize the **Auth Client**

    
    ```go
    package config
    
    import (
        "context"
        "os"
        "log"
    
        firebase "firebase.google.com/go"
        "firebase.google.com/go/auth"
    )
    
    func InitFirebase() *firebase.App {
        conf := &firebase.Config{ProjectID: os.Getenv("GCP_PROJECT_ID")}
        app, err := firebase.NewApp(context.Background(), conf)
        if err != nil {
            log.Printf("error initializing app: %v\n", err)
            return nil
        }
    
        return app
    }
    
    func InitFirebaseAuth(app *firebase.App) *auth.Client {
        client, err := app.Auth(context.Background())
        if err != nil {
            log.Printf("error initializing firebase auth client: %v\n", err)
            return nil
        }
    
        return client
    }
    ```

    In some cases, you might *not* have to specify **ProjectID**, but I strongly recommend you do to avoid inter-project accidents.

2. Next, add the **Auth** middleware as shown below

    ```go
    package middlewares
    
    import (
        "log"
        "strings"
        "context"
    
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
        "firebase.google.com/go/auth"
    )
    
    type AuthClient interface {
    	VerifyIDToken(ctx context.Context, idToken string) (*auth.Token, error)
    }
    
    func UseAuth(authClient AuthClient) fiber.Handler {
        return func(c *fiber.Ctx) error {
            log.Println("In Auth middleware")
    
            authHeader := c.Get("Authorization")
            if authHeader == "" {
                log.Println("Missing Authorization header")
                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                    "message": "Missing Authorization header",
                })
            }
    
            // Bearer token split to get the token without "Bearer" in front
            val := strings.Split(authHeader, " ")
            if len(val) != 2 {
                log.Println("Invalid Authorization header")
                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                    "message": "Invalid Authorization header",
                })
            }
            token := val[1]
    
            decodedToken, err := authClient.VerifyIDToken(c.Context(), token)
            if err != nil {
                log.Printf("Error verifying token. Error: %v\n", err)
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Invalid authentication token",
                })
            }
    
            // Only needed for untrusted providers (e.g. email/password)
            // But I suggest verifying it for all providers
            if decodedToken.Claims["email_verified"] != true {
                log.Println("Email not verified")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Email not verified",
                })
            }
    
            if _, ok := decodedToken.Claims["role"]; !ok {
                log.Println("Role not present in token")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Role not present in token",
                })
            }
    
            user := &m.User{
                UID: decodedToken.UID,
                Email: decodedToken.Claims["email"].(string),
                Role: decodedToken.Claims["role"].(string),
            }
            c.Locals("user", user)
    
            log.Println("Successfully authenticated")
            log.Printf("Email: %v\n", user.Email)
            log.Printf("Role: %v\n", user.Role)
    
            return c.Next()
        }
    }
    ```

    1. Extract the token from the Authorization header
    2. Pass the token to the `authClient.VerifyIDToken()` interface method for verification (**AuthClient** will shortly be substituted by Firebase’s Auth Client)
    3. The user data, including the **role** claim, are stored in the **Locals** for the next middleware or handler to use

3. Add the **AuthZ** (authorization) middleware

    ```go
    package middlewares
    
    import (
        "log"
    
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
    )
    
    func UseAuthZ(requiredRole string) fiber.Handler {
        return func(c *fiber.Ctx) error {
            log.Println("In AuthZ middleware")
    
            user, ok := c.Locals("user").(*m.User)
            if !ok {
                log.Println("User not found in context")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Unauthorized",
                })
            }
    
            if user.Role == "" {
                log.Println("User role not set")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Unauthorized",
                })
            }
    
            if user.Role != requiredRole {
                log.Printf("User with email %s and role %s tried to access " +
                    "a route that was for the %s role only",
                    user.Email, user.Role, requiredRole)
                return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
                    "message": "Forbidden",
                })
            }
    
            log.Printf("User with email %s and role %s authorized",
                user.Email, user.Role)
    
            return c.Next()
        }
    }
    ```

    The `if user.Role != requiredRole` implementation is quite primitive. It is up to you to define the logical authorization rules at this point (e.g. you might want to allow the **admin** to access any resource).

4. Lastly, we have to put the flow together in the `main.go`

    ```go
    package main
    
    import (
        "os"
        "log"
    
        c "middleware-go/src/config"
        mw "middleware-go/src/middlewares"
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
    )
    
    func userHandler(c *fiber.Ctx) error {
        user := c.Locals("user").(*m.User)
        return c.JSON(user)
    }
    
    func main() {
        app := fiber.New()
    
        firebaseApp := c.InitFirebase()
        authClient := c.InitFirebaseAuth(firebaseApp)
    
        // only authenticated admins can access this route
        app.Get("/user", mw.UseAuth(authClient), mw.UseAuthZ("admin"), userHandler)
    
        log.Fatal(app.Listen(":" + os.Getenv("PORT")))
    }
    ```
    
    1. Initialize the Firebase auth client at this level
    2. Pass the client to the **Auth** middleware
    3. Specify the role you want your **AuthZ** middleware to allow for this route
    4. If the entire flow passes (the user is a verified admin), execute the handler logic

**Run Locally**

God bless Docker, because with [**docker-compose**](https://docs.docker.com/compose/gettingstarted/), you simply:

1. pull **[the repo](https://github.com/MichalMoravik/google-identity-guide)**
2. `cd middleware-go`
3. change the environment variables in the **docker-compose.yml** file
    
    The **GOOGLE_APPLICATION_CREDENTIALS** have to be mapped to the container. There is a quite good [Google docs page](https://cloud.google.com/docs/authentication/application-default-credentials) explaining how to obtain the file. I use [the default credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal) to avoid hassle.
    
4. run `docker-compose up`, and the service should be up and running, ready to receive requests

It is 2023; I’m not gonna explain any other way of running this.

**Test with Token**

1. To get the token, go to your Client code and search for commented-out lines
    in React / Next client → **app/page.js**
    in Vanilla JS → **both files**
2. Uncomment the lines and sign in (try both - admin and non-admin accounts)
3. Copy the token printed in the browser console
4. Open HTTP client (curl, Postman, Insomnia, …)
5. Set **Authorization** header with the value **Bearer <token>**
6. Send a GET request to **http://127.0.0.1:8089/user**
7. When testing with the **admin** token, you should see the user data in the response

**Deploy**

The quickest way is to deploy to [Cloud Run](https://cloud.google.com/run). The service will use **Dockerfile** to build the production image. You can even omit the **GOOGLE_APPLICATION_CREDENTIALS** env var as these are in GCP’s projects by default. 

&nbsp;

> ❕ [**Jump to Wrap up Section**](#wrap-up)

&nbsp;

#### Node Middlewares

**Set Up**

Install Express and Firebase Admin SDK (`npm i firebase-admin`).

> ❔ **Firebase Admin SDK**
> You’ll use this SDK anytime you want your **backend** to communicate with Google Identity Platform. Again, in this case, Google decided to reuse the already developed Firebase library rather than making a new one specifically for Identity Platform.

**Implement**

1. Let the SDK know what project you are using by setting up **Firebase App**. Then export the **Auth Client**

    ```js
    import admin from 'firebase-admin';
    
    admin.initializeApp({
      credential: admin.credential.applicationDefault(),
      projectId: process.env.GCP_PROJECT_ID,
    });
    
    const authClient = admin.auth();
    
    export { authClient };
    ```
    
    In some cases, you might *not* have to specify **ProjectID**, but I strongly recommend you do to avoid inter-project accidents.

2. Next, add the **Auth** middleware as shown below

    ```js
    function useAuth(authClient) {
      return async function(req, res, next) {
        console.log('In Auth middleware');
    
        const authHeader = req.headers.authorization;
        if (!authHeader) {
          console.log('Missing Authorization header');
          return res.status(400).json({ message: 'Missing Authorization header' });
        }
    
        const [bearer, token] = authHeader.split(' ');
        if (bearer !== 'Bearer' || !token) {
          console.log('Invalid Authorization header');
          return res.status(400).json({ message: 'Invalid Authorization header' });
        }
    
        let decodedToken;
        try {
          decodedToken = await authClient.verifyIdToken(token);
        } catch (error) {
          console.log(`Error verifying token. Error: ${error}`);
          return res.status(401).json({ message: 'Invalid authentication token' });
        }
    
        // Only needed for untrusted providers (e.g. email/password)
        // But I suggest verifying it for all providers
        if (!decodedToken.email_verified) {
          console.log('Email not verified');
          return res.status(401).json({ message: 'Email not verified' });
        }
    
        if (!decodedToken.role) {
          console.log('Role not present in token');
          return res.status(401).json({ message: 'Role not present in token' });
        }
    
        const user = {
          uid: decodedToken.uid,
          email: decodedToken.email,
          role: decodedToken.role,
        };
        res.locals.user = user;
    
        console.log('Successfully authenticated');
        console.log(`Email: ${user.email}`);
        console.log(`Role: ${user.role}`);
    
        next();
      };
    }
    
    export { useAuth };
    ```
    
    1. Extract the token from the Authorization header
    2. Pass the token to the `authClient.verifyIdToken()` function for verification
    3. The decoded user data, including the **role** claim, are stored in the **locals** for the next middleware or handler to use

3. Add the **AuthZ** (authorization) middleware

    ```js
    function useAuthZ(requiredRole) {
      return function(req, res, next) {
        console.log('In AuthZ middleware');
    
        const user = res.locals.user;
        if (!user) {
          console.log('User not found in context');
          return res.status(401).json({ message: 'Unauthorized' });
        }
    
        if (!user.role) {
          console.log('User role not set');
          return res.status(401).json({ message: 'Unauthorized' });
        }
    
        if (user.role !== requiredRole) {
          console.log(`User with email ${user.email} and role ${user.role} tried to access a route that was for the ${requiredRole} role only`);
          return res.status(403).json({ message: 'Forbidden' });
        }
    
        console.log(`User with email ${user.email} and role ${user.role} authorized`);
    
        next();
      };
    }
    
    export { useAuthZ };
    ```
    
    The `if (user.role !== requiredRole)` implementation is quite primitive. It is up to you to define the logical authorization rules at this point (e.g. you might want to allow the **admin** to access any resource).

4. Lastly, we have to put the flow together in the `index.js`
    
    ```js
    import express from 'express';
    import { authClient } from './firebase.js';
    import { useAuth } from './auth.js';
    import { useAuthZ } from './authz.js';
    
    const app = express();
    
    app.get('/user', useAuth(authClient), useAuthZ('admin'), (req, res) => {
      const user = res.locals.user;
      res.json(user);
    });
    
    const port = process.env.PORT;
    app.listen(port, () => {
      console.log(`Server is running on port ${port}`);
    });
    ``` 

    1. Pass the client to the **Auth** middleware
    2. Specify the role you want your **AuthZ** middleware to allow for this route
    3. If the entire flow passes (the user is a verified admin), execute the handler logic

**Run Locally**

God bless Docker, because with [**docker-compose**](https://docs.docker.com/compose/gettingstarted/), you simply:

1. pull **[the repo](https://github.com/MichalMoravik/google-identity-guide)**
2. `cd middleware-node`
3. change the environment variables in the **docker-compose.yml** file
    
    The **GOOGLE_APPLICATION_CREDENTIALS** have to be mapped to the container. There is a quite good [Google docs page](https://cloud.google.com/docs/authentication/application-default-credentials) explaining how to obtain the file. I use [the default credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal) to avoid hassle.
    
4. run `docker-compose up`, and the service should be up and running, ready to receive requests

It is 2023; I’m not gonna explain any other way of running this.

**Test with Token**

1. To get the token, go to your Client code and search for commented-out lines
    in React / Next client → **app/page.js**
    in Vanilla JS → **both files**
2. Uncomment the lines and sign in (try both - admin and non-admin accounts)
3. Copy the token printed in the browser console
4. Use HTTP client (curl, Postman, Insomnia, …)
5. Set **Authorization** header with the value **Bearer <token>**
6. Send a GET request to **http://localhost:8088/user**
7. When testing with the **admin’s** token, you’ll see the user data in the response

**Deploy**

The quickest way is to deploy to [Cloud Run](https://cloud.google.com/run). The service will use **Dockerfile** to build the production image. You can even omit **GOOGLE_APPLICATION_CREDENTIALS** env var as these are in GCP’s projects by default. 

&nbsp;

## Wrap Up

![Full Flow Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tucifv7uq52518o2qa8l.png)

You have now the full overview of the flow Google recommends using whenever you want to control access using Identity Platform. 

My plan for 2023 is to build projects in public and write articles. If you want to see my next moves, follow me on [x.com](https://twitter.com/MoravikMichal) (Twitter).