HomeAbout MeBook a Call

Secure Google Workspace Addons with Firebase Auth A GDE Blueprint

By Vo Tu Duc
May 05, 2026
Secure Google Workspace Addons with Firebase Auth A GDE Blueprint

While getting the user’s email in Apps Script is simple for user-initiated actions, this method breaks down with automated triggers. Discover how to solve this critical identity challenge for your advanced automations.

image 0

The Identity Challenge Beyond Standard Workspace Triggers

When you first start building a [Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in [Automated Web Scraping with [Multilingual Text-to-Speech Tool with SocialSheet Streamline Your Social Media Posting 123](https://votuduc.com/Multilingual-Text-to-Speech-Tool-with-Google-Workspace-p809282)](https://votuduc.com/Automated-Web-Scraping-with-Google-Sheets-p292968)](https://workspace.google.com/marketplace/app/auto_create_folder_and_files/430076014869) Add-on, the platform provides a beautifully simple way to identify the current user. With a single line of Apps Script, like Session.getActiveUser().getEmail(), you get the user’s email address. This is perfect for simple, context-aware scripts that perform actions on behalf of the user who triggered them. For example, creating a draft in their Gmail or adding an event to their Calendar.

However, the moment your add-on ambitions grow beyond a simple utility and into the realm of a scalable Software-as-a-Service (SaaS) product, this convenient, trigger-based identity model reveals its limitations. It’s a foundation built for quicksand, not skyscrapers. To build a robust, secure, and feature-rich application, you need to think beyond the immediate context of the add-on trigger and establish an independent, verifiable source of truth for user identity.

Why default user identity isn’t enough for a scalable SaaS

Relying solely on the user information provided by the add-on event object (e.user.email) presents several critical challenges that can compromise security, scalability, and functionality.

  1. It’s Context-Bound and Ephemeral: The identity is only available when a user actively interacts with your add-on, triggering a function. What happens when your backend needs to perform a background task for that user hours later, like processing a large imported file or sending a scheduled report? The add-on’s execution context is long gone, and your backend has no secure way to re-establish the user’s identity or act on their behalf.

  2. It’s Fundamentally Insecure for Backend APIs: Sending the user’s email from the add-on’s frontend (Apps Script) to your backend API is not a secure authentication mechanism. It’s just a piece of data. A malicious actor could easily craft a request to your API endpoint, insert any email address they want, and potentially access or manipulate another user’s data.

image 1
  1. **It Lacks Application-Specific Context: A AC2F Streamline Your Google Drive Workflow identity knows a user’s email, but it knows nothing about their relationship with your application. Is this user on the “Free” or “Pro” plan? What organization or tenant do they belong to in your multi-tenant system? What are their specific roles and permissions (e.g., admin, editor, viewer) within your app? Embedding this business logic into every API call based on a simple email lookup is inefficient and difficult to manage.

  2. It Hinders Multi-Platform Expansion: What if you want to offer a web app or a mobile app alongside your Workspace Add-on? A user identity tied exclusively to the add-on’s session forces you to build entirely separate authentication systems for each platform, leading to a fragmented user experience and duplicated engineering effort.

Introducing a decoupled identity model with Firebase Auth

The solution is to decouple your application’s identity management from the Automated Client Onboarding with Google Forms and Google Drive. execution context. Instead of implicitly trusting the identity provided by the add-on trigger, you establish an independent, authoritative authentication layer. This is precisely where Firebase Authentication shines.

By integrating Firebase Auth, you create a centralized identity hub for your application. Here’s how it transforms the model:

  • Verifiable Identity via JWTs: When a user signs into your add-on via Firebase, it generates a JSON Web Token (JWT). This is a secure, digitally signed token that the add-on sends to your backend with every API request. Your backend can then cryptographically verify the token’s signature, guaranteeing that the request is authentic and unmodified. This single-handedly solves the API security problem.

  • Centralized User Management: Firebase Auth becomes the single source of truth for who your users are. You get a robust backend and SDKs for managing user lifecycles, handling password resets, and integrating multiple sign-in methods (Google, email/password, social providers, etc.). This provides a unified identity system that works across your Workspace Add-on, web app, and mobile clients.

  • Rich Context with Custom Claims: This is a superpower for SaaS applications. Firebase Auth allows you to embed application-specific data—known as “custom claims”—directly into the user’s JWT. You can add key-value pairs like subscriptionTier: 'pro', tenantId: 'acme-corp', or roles: ['admin', 'editor']. When your backend verifies the JWT, it gets this information instantly and securely, without needing an extra database lookup. This makes it incredibly efficient to enforce permissions and tailor the user experience.

This decoupled model doesn’t replace the Google identity; it enhances it. The user still enjoys a seamless “Sign in with Google” experience, but under the hood, you are establishing a far more powerful and secure session with your own application’s ecosystem.

What you’ll build with this GDE blueprint

In the following sections, we will move from theory to practice. We won’t just talk about concepts; we will build a complete, end-to-end solution that implements this robust authentication pattern. By following this blueprint, you will construct:

  • A Secure Add-on Frontend: You’ll build a Card Service UI that initiates a proper OAuth2 flow, directing the user to sign in with their Google Account through your Firebase project.

  • A Token-Based Communication Channel: You’ll learn how the add-on securely receives the Firebase ID Token (JWT) after a successful login and includes it as a Bearer token in the Authorization header for all subsequent calls to your backend.

  • A Protected Backend API: We’ll deploy a Google Cloud Function that serves as your add-on’s backend. This function will include essential middleware to intercept every incoming request, validate the Firebase JWT, and safely extract the user’s identity and custom claims.

  • A Complete End-to-End Flow: You will see the entire authentication and data-fetching process in action, from the user clicking a button in Google Docs to the add-on displaying data securely retrieved from your protected backend.

By the end of this guide, you’ll have a production-ready authentication pattern that sets the stage for a truly scalable and secure SaaS application built on top of Automated Discount Code Management System.

Core Architecture: A Custom OAuth Flow for Your Add-on

Before we dive into the code, it’s crucial to understand the architectural blueprint. We aren’t just plugging in a library; we’re orchestrating a secure dance between Automated Email Journey with Google Sheets and Google Analytics, your own backend services, and a robust identity provider. The goal is to create a seamless user experience where a user’s Google identity is securely linked to their identity within your application, all managed by Firebase. This is accomplished using a custom implementation of the OAuth 2.0 Authorization Code flow, tailored specifically for the Apps Script environment.

High-level diagram of the authentication flow

Visualizing the flow is the best way to grasp how the different services interact. At its core, the add-on initiates a process that lets the user sign into your application (via Firebase) using their Google account, which then passes a secure token back to the add-on to authorize subsequent actions.

Here’s a sequence diagram outlining the entire journey:


sequenceDiagram

participant User

participant Add-on UI (Client-side)

participant Apps Script (Server-side)

participant Auth Web App (Your Service)

participant Firebase Auth

User->>Add-on UI: Opens Add-on

Add-on UI->>Apps Script: checkAuth()

Apps Script-->>Add-on UI: Not authenticated, returns authUrl

Add-on UI->>User: Renders "Authorize" button

User->>Add-on UI: Clicks "Authorize"

Add-on UI->>User: Opens new tab to Auth Web App (with authUrl)

User->>Auth Web App: Arrives at login page

Auth Web App->>Firebase Auth: Initiates Google Sign-In (FirebaseUI)

Firebase Auth->>User: Shows Google Sign-In prompt

User->>Firebase Auth: Authenticates with Google account

Firebase Auth-->>Auth Web App: Returns Firebase ID Token

Auth Web App->>Auth Web App: Verifies ID Token, sets custom claims (optional)

Auth Web App->>Apps Script: Redirects to callback function with ID Token

Apps Script->>Firebase Auth: Verifies the received ID Token

Note over Apps Script: This is a critical security step!

Apps Script->>Apps Script: Stores verified token in PropertiesService

Apps Script-->>User: Renders "Success! You can close this tab."

User->>Add-on UI: Returns to Workspace, reloads add-on

Add-on UI->>Apps Script: checkAuth()

Apps Script->>Apps Script: Retrieves token from PropertiesService

Apps Script-->>Add-on UI: Authenticated! Returns user data.

Add-on UI->>User: Displays authenticated add-on interface

This flow ensures that at no point are we handling user passwords. We leverage Google’s own authentication as the source of truth, and Firebase acts as the broker, issuing its own verifiable tokens (JWTs) that our add-on and backend can trust.

Key components: Firebase Auth, Apps Script, and GCP Identity Platform

To build this, we rely on three core Google technologies working in concert.

  1. Firebase Authentication: This is our primary Identity Provider (IdP). It provides the complete backend service, easy-to-use SDKs, and pre-built UI libraries (FirebaseUI) to authenticate users. For our use case, we’ll configure it to use “Google” as a sign-in provider, creating a frictionless experience. Its main jobs are:
  • Managing the user pool for our application.

  • Handling the sign-in flow securely.

  • Issuing signed JSON Web Tokens (JWTs) upon successful authentication.

  • Providing the Admin SDK for server-side token verification and user management.

  1. Apps Script: This is the server-side backend for our Automated Google Slides Generation with Text Replacement Add-on. It’s not just for automating Sheets or Docs; it’s a capable serverless platform. In our architecture, it plays several critical roles in the OAuth flow:
  • OAuth Client: It initiates the authentication request.

  • State Management: It generates and validates a state token to prevent Cross-Site Request Forgery (CSRF) attacks.

  • Callback Endpoint: It exposes a public web app URL that serves as the redirect_uri for the OAuth flow. This is the doGet() function that receives the token from our Auth Web App.

  • Secure Storage: It uses the PropertiesService to securely store the user-specific Firebase ID token and refresh token, scoping them to the user and the script.

  • Authenticated Middleware: It attaches the stored ID token as a Bearer token to any outbound API calls to our own backend services.

  1. GCP Identity Platform: It’s important to know that Firebase Auth is, in fact, a user-friendly layer built directly on top of the powerful Google Cloud Identity Platform. For most use cases, you’ll interact directly with the Firebase console and SDKs. However, when you need more advanced, enterprise-grade features like multi-tenancy, SAML/OIDC federation, or integration with other GCP services, you’re tapping into the power of Identity Platform. Think of it this way: Firebase gets you started quickly, and Identity Platform provides the path for scaling to complex identity requirements.

The role of custom claims in managing user permissions

Simply knowing who a user is (authentication) is only half the battle. We also need to know what they are allowed to do (authorization). This is where Firebase custom claims become an incredibly powerful tool.

Custom claims are key-value pairs that you can embed directly into a user’s Firebase ID token. This is done securely from a trusted backend environment (like a Cloud Function or your own server) using the Firebase Admin SDK.

Why is this better than a database lookup?

Imagine your add-on needs to show an “Admin Panel” button only to users with an admin role. The traditional approach would be:

  1. User opens the add-on.

  2. Apps Script verifies the user’s ID token.

  3. Apps Script makes a separate API call to your database: GET /api/users/{userId}/permissions.

  4. The API responds with the user’s roles.

  5. Apps Script processes the response and tells the UI to render the button.

With custom claims, the flow is much more efficient and secure:

  1. User opens the add-on.

  2. Apps Script gets the ID token from PropertiesService.

  3. The token is decoded and verified. The permissions are already inside the token’s payload.

  4. Apps Script immediately knows the user is an admin and tells the UI to render the button.

The claims are cryptographically signed as part of the JWT, meaning they cannot be tampered with by the client. They become a self-contained, portable, and verifiable “passport” of the user’s identity and permissions.

Here’s a quick example of how you might set a claim using the Firebase Admin SDK in a Node.js environment (e.g., a Cloud Function that triggers when a user is assigned a role in your system):


// In a secure backend environment (e.g., Cloud Function)

import { getAuth } from 'firebase-admin/auth';

const userId = 'some-user-uid'; // The UID of the user to modify

await getAuth().setCustomUserClaims(userId, {

admin: true,

plan: 'premium'

});

Now, the next time this user signs in or their ID token is refreshed, the decoded token payload will contain:


{

"iss": "https://securetoken.google.com/your-project-id",

"aud": "your-project-id",

"auth_time": 1670000000,

"user_id": "some-user-uid",

"sub": "some-user-uid",

"iat": 1670000100,

"exp": 1670003700,

"email": "[email protected]",

"email_verified": true,

"firebase": { ... },

// --- Our Custom Claims ---

"admin": true,

"plan": "premium"

}

Your Apps Script code can then inspect these claims to make authorization decisions instantly, creating a more responsive and secure add-on.

Step 1: Setting Up Your Firebase and GCP Environment

Before we write a single line of addon code, we must lay a solid foundation. This involves creating and configuring a Firebase project, which is intrinsically linked to a Google Cloud Platform (GCP) project. Getting these settings right from the start is critical for a secure and functional authentication flow.

Configuring a new Firebase project and enabling Identity Platform

Our journey begins in the Firebase console. Firebase will serve as the central hub for managing our application’s authentication.

  1. Create the Firebase Project:

Click on* “Add project”**.

  • Give your project a descriptive name, such as workspace-addon-auth-demo. Firebase will automatically generate a unique Project ID. You can also link this to an existing Google Cloud project if you have one prepared.

  • You’ll be prompted to enable Google Analytics. While beneficial for production apps, it’s not required for this blueprint. You can choose to enable it or skip it.

Click* “Create project”** and wait for Firebase to provision your resources.

  1. Enable Authentication:

Once your project dashboard loads, navigate to the* Build section in the left-hand menu and click on Authentication**.

Click the* “Get started”** button. This action enables the basic Firebase Authentication service for your project.

  1. Upgrade to Google Cloud Identity Platform:

Firebase Authentication is powerful, but Identity Platform is its enterprise-grade superset. It provides advanced features like multi-tenancy and broader protocol support (SAML, OIDC), which are often essential for Workspace addons targeting business clients. Enabling it is a no-cost upgrade that unlocks significant capabilities.

  • Navigate to your underlying Google Cloud Platform project. You can get there quickly by going to your Firebase Project Settings (⚙️ icon) and clicking the link under “Project ID” on the General tab.

In the GCP console, use the search bar at the top to find and select* “Identity Platform”**.

If it’s not already enabled, click the* “Enable”** button. This transparently upgrades your Firebase Auth backend without affecting your existing configuration.

With this, you have a robust authentication backend ready to be configured.

Setting up authorized domains and OAuth redirect URIs

For security, we must explicitly tell our authentication provider which domains are allowed to initiate login flows and where users should be sent back after a successful login. This prevents malicious actors from using your credentials on their own sites.

  1. Authorize the Apps Script Domain:

Your addon’s UI and server-side logic run on Google’s infrastructure. We need to add its domain to the allowlist.

Return to the* Firebase console**.

Go to* Authentication** -> Settings tab.

Select the* Authorized domains** section.

Click* “Add domain”** and enter: script.google.com

Click* “Add”**.

  1. Configure the OAuth Redirect URI:

After a user authenticates (e.g., with their Google account), the provider needs a specific URL to redirect them back to your application to complete the process. In Apps Script, this URL is unique to your script project.

First, you need your* Script ID**. Open your [Automated Order Processing Wordpress to Gmail to Google Sheets to Real Time Jobber and Google Sheets Integration](https://votuduc.com/Automated-Order-Processing-Wordpress-to-Gmail-to-Google-Sheets-to-Jobber-p649487) Addon project in the Apps Script editor. Click on the Project Settings (⚙️) icon in the left sidebar. Copy the Script ID from the “IDs” section.

Next, navigate to the* Google Cloud Console** for your project.

Go to* APIs & Services** -> Credentials.

  • Under the “OAuth 2.0 Client IDs” section, you will see a web client that was automatically created by Google services for your Firebase project. It’s often named “Web client (auto created by Google Service)“. Click the pencil icon (✏️) to edit it.

Under the* “Authorized redirect URIs” section, click ”+ ADD URI”**.

  • Paste the following URL, replacing {YOUR_SCRIPT_ID} with the ID you copied earlier:

https://script.google.com/macros/d/{YOUR_SCRIPT_ID}/usercallback

Click* “Save”**. This critical step ensures the OAuth 2.0 flow can be successfully completed within the Apps Script environment.

Essential Apps Script project properties for storing credentials

You must never hardcode secrets like API keys or client secrets directly in your .gs files. Apps Script provides a secure mechanism called Script Properties for storing this sensitive data.

  1. Gather Your Credentials:

You’ll need four key pieces of information:

  • Firebase API Key & Auth Domain: In the Firebase Console, go to Project Settings (⚙️) -> General tab. In the “Your apps” card, find your Web App and look for the firebaseConfig object. Copy the values for apiKey and authDomain.

  • OAuth Client ID & Client Secret: In the Google Cloud Console, go to APIs & Services -> Credentials. Find the same Web client you edited in the previous step. Copy the “Client ID” and “Client secret”.

  1. Store in Script Properties:

Return to the* Apps Script editor**.

Go to* Project Settings** (⚙️).

Scroll down to the* “Script Properties” section and click “Edit script properties”**.

Click* “Add script property”** four times to create the following key/value pairs, pasting the credentials you just gathered:

| Property | Value |

| --------------------- | -------------------------------------- |

| FIREBASE_API_KEY | Your apiKey from the Firebase config |

| FIREBASE_AUTH_DOMAIN| Your authDomain from the Firebase config |

| OAUTH_CLIENT_ID | Your Client ID from GCP Credentials |

| OAUTH_CLIENT_SECRET | Your Client secret from GCP Credentials|

Click* “Save script properties”**.

By using Script Properties, your credentials are securely stored with the project, are accessible only via the PropertiesService API, and won’t be exposed if you share your code or check it into a version control system. Your environment is now fully configured and ready for the next step.

Step 2 Implementing the Apps Script OAuth2 Service

With our Firebase and Google Cloud configurations in place, it’s time to dive into the Apps Script code. The core of our client-side authentication flow will be managed by the fantastic OAuth2 for Apps Script library. This library is the de facto standard for handling OAuth2 flows in the Apps Script environment, abstracting away the tedious and error-prone details of token management, redirects, and state verification.

First, you’ll need to add this library to your Apps Script project.

  1. In the Apps Script editor, click the + icon next to “Libraries”.

  2. Enter the following Script ID: 1B7FSrk57A1B1LCo3jCeL9DajUaxj72B62vMYkGmcXjLwDEmU5UFl-4kX

  3. Click “Look up” and select the latest version.

  4. Leave the identifier as OAuth2 and click “Add”.

Now that the library is included, we can build the service that will orchestrate the entire authentication process.

Building the service to handle the authorization endpoint

The first step is to create and configure an OAuth2 service object. This object holds all the necessary information to communicate with Google’s OAuth 2.0 endpoints on behalf of Firebase Auth, including your client credentials, the required scopes, and the all-important callback function.

We’ll wrap this configuration in a function, getFirebaseAuthService(), to ensure we can easily access our configured service from anywhere in our script.


/**

* Creates and configures the OAuth2 service for Firebase Authentication

* using Google as the identity provider.

* @returns {OAuth2.Service} The configured OAuth2 service.

*/

function getFirebaseAuthService() {

// IMPORTANT: Store your client ID and secret in Script Properties for security.

// Go to Project Settings > Script Properties and add them.

const userProps = PropertiesService.getUserProperties();

const scriptProps = PropertiesService.getScriptProperties();

const clientId = scriptProps.getProperty('CLIENT_ID');

const clientSecret = scriptProps.getProperty('CLIENT_SECRET');

if (!clientId || !clientSecret) {

throw new Error('CLIENT_ID or CLIENT_SECRET is not set in Script Properties.');

}

return OAuth2.createService('firebaseAuth')

// Set the authorization URL. For Google Sign-In, this is the standard Google endpoint.

.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/v2/auth')

// Set the token URL.

.setTokenUrl('https://oauth2.googleapis.com/token')

// Set the client ID and secret from your GCP project.

.setClientId(clientId)

.setClientSecret(clientSecret)

// Set the name of the callback function that will be invoked after authorization.

.setCallbackFunction('authCallback')

// Set the property store where user tokens will be stored.

// UserProperties is used to store data on a per-user basis.

.setPropertyStore(userProps)

// Set the cache to use for temporary data storage.

.setCache(CacheService.getUserCache())

// Use LockService to prevent concurrent executions from interfering with each other.

.setLock(LockService.getUserLock())

// Request the necessary scopes for Firebase Auth with Google.

// 'openid', 'email', and 'profile' are required to get an ID token.

.setScope('openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile')

// These parameters are crucial for the add-on flow.

.setParam('login_hint', Session.getActiveUser().getEmail())

.setParam('prompt', 'consent') // Forces the consent screen, essential for getting a refresh token.

.setParam('access_type', 'offline'); // Ensures a refresh token is returned.

}

Let’s break down this configuration:

  • createService('firebaseAuth'): We initialize the service with a unique name. This name is used as a key when storing tokens in PropertiesService.

  • setAuthorizationBaseUrl & setTokenUrl: These are the standard Google OAuth 2.0 endpoints. Since we’re using Google as our Firebase identity provider, these are the correct URLs to use.

  • setClientId & setClientSecret: Here, we pull the credentials from ScriptProperties. Never hardcode secrets in your code. Storing them in Script Properties provides a basic level of security by keeping them out of your source control.

  • setCallbackFunction('authCallback'): This is a critical step. We’re telling the library to execute a function named authCallback once the user has completed the authorization flow in the pop-up window.

  • setPropertyStore(PropertiesService.getUserProperties()): We instruct the library to store the user’s tokens in their personal UserProperties store. This is essential for security and multi-user functionality.

  • setScope(...): We request the openid, email, and profile scopes. This is the standard set required to authenticate a user and receive an ID token, which is what Firebase needs.

  • setParam(...):

  • login_hint: Pre-fills the user’s email address in the sign-in prompt for a smoother UX.

  • prompt: 'consent' & access_type: 'offline': This combination is vital. It ensures that Google’s consent screen is always shown on the first authorization and, most importantly, guarantees that we receive a refresh token. Without a refresh token, the user would have to re-authorize every hour, which is a terrible user experience.

Creating the callback function to process the authorization code

When a user clicks your “Sign In” button, they are redirected to the authorization URL. After they approve the permissions, Google redirects them back to a special URL associated with your script. The OAuth2 library intercepts this redirect and invokes the callback function we specified: authCallback.

The purpose of this function is to handle the final step of the OAuth2 dance: exchanging the authorization code for access and refresh tokens. The library does all the hard work; we just need to call the right method and provide feedback to the user.


/**

* The callback function that is invoked after the user completes the

* OAuth2 authorization flow.

* @param {object} request The request object from the callback URL.

* @returns {HtmlOutput} An HTML page to be displayed to the user.

*/

function authCallback(request) {

const service = getFirebaseAuthService();

const isAuthorized = service.handleCallback(request);

if (isAuthorized) {

return HtmlService.createHtmlOutput(

'<script>setTimeout(function() { top.close() }, 1);</script>' +

'Success! You can close this tab and return to the add-on.'

);

} else {

return HtmlService.createHtmlOutput('Access Denied. You can close this tab.');

}

}

This function is elegantly simple:

  1. It retrieves our configured firebaseAuth service.

  2. It calls service.handleCallback(request). This method takes the incoming request parameters, extracts the authorization code, and automatically communicates with the tokenUrl to exchange it for the tokens.

  3. The library then securely stores these tokens using the PropertyStore we defined earlier.

  4. Finally, we return a simple HTML message to the user in the pop-up window. The small script top.close() attempts to automatically close the window for a seamless experience, though browser security settings may sometimes prevent this.

Securely storing and managing user tokens with PropertiesService

Security is paramount when dealing with authentication tokens. A leaked refresh token could grant an attacker long-term access to a user’s account. The Apps Script OAuth2 library, combined with PropertiesService, provides a robust and secure storage mechanism.

How it Works:

When you configure the service with .setPropertyStore(PropertiesService.getUserProperties()), you are leveraging a key-value store that is sandboxed for each user of your script.

  • User-Specific Storage: PropertiesService.getUserProperties() ensures that User A’s tokens are stored in a place completely inaccessible to User B, even when they are using the same add-on. This is the single most important principle for secure token storage in a multi-user environment.

  • Automatic Encryption: The OAuth2 library does not store your tokens in plain text. It automatically encrypts the token data before saving it to the PropertiesService, adding a crucial layer of security. The library manages the encryption keys internally.

  • Effortless Token Management: The true beauty of this setup is that you rarely need to interact with the tokens directly.

  • Retrieval: To get a valid access token, you simply call getFirebaseAuthService().getAccessToken(). The library checks if the stored token is expired. If it is, it will automatically use the stored refresh token to get a new access token, store it, and then return it to you. This entire refresh flow is completely transparent to your code.

  • Logout/Revocation: To log a user out and delete their stored credentials, you simply call getFirebaseAuthService().reset(). This securely wipes the tokens for that specific user from their UserProperties store.

Step 3 Building the User-Facing Authentication Card

With our server-side OAuth2 plumbing in place, it’s time to build the user interface. In a Automated Payment Transaction Ledger with Google Sheets and PayPal Add-on, the UI is constructed using a series of “Cards.” Our goal is to create a dynamic interface that presents a login prompt to unauthenticated users and a welcome message with a sign-out option to those who are already authenticated.

Designing a simple and secure login interface in Apps Script

The first impression your add-on makes is its interface. For authentication, clarity and trust are paramount. We’ll create a simple card that explains why the user needs to sign in and provides a clear call-to-action.

We use Apps Script’s CardService to build this UI. The key component is a Button widget configured with a special setAuthorizationAction. This action tells the Google Docs to Web host (Gmail, Docs, etc.) to initiate the OAuth2 flow we defined in the previous step. It’s the most secure way to start this process, as it leverages Google’s trusted UI for the authorization pop-up.

Let’s create a function createLoginCard() that generates this interface.


/**

* Creates a card that prompts the user to authorize the add-on.

* @returns {Card} The authorization card.

*/

function createLoginCard() {

// Get the authorization URL from our OAuth2 service.

const authorizationUrl = getOAuthService().getAuthorizationUrl();

// Build the card section with a descriptive text and a login button.

const loginSection = CardService.newCardSection()

.addWidget(CardService.newTextParagraph()

.setText('To get started, please connect your Google Account. This will allow the add-on to securely identify you with our service.')

)

.addWidget(CardService.newButton()

.setText('Sign in with Google')

.setAuthorizationAction(CardService.newAuthorizationAction()

.setAuthorizationUrl(authorizationUrl))

);

// Assemble and return the final card.

const card = CardService.newCardBuilder()

.setHeader(CardService.newCardHeader().setTitle('Authentication Required'))

.addSection(loginSection)

.build();

return card;

}

Breaking it down:

  • getOAuthService().getAuthorizationUrl(): This retrieves the fully-formed authorization URL, complete with client_id, redirect_uri, scope, and other parameters we configured earlier.

  • CardService.newTextParagraph(): We provide clear context for the user. Never present a login button without explaining why it’s needed.

  • setAuthorizationAction(): This is the magic. Instead of a standard setOnClickAction that calls another Apps Script function, this action tells the host application to open the authorizationUrl in a pop-up dialog. This is crucial for security and a seamless user experience.

Handling user interactions and initiating the OAuth flow

When a user clicks the “Sign in with Google” button, the following sequence of events is triggered, orchestrated by SocialSheet Streamline Your Social Media Posting and our Apps Script code:

  1. User Click: The user clicks the button on the card.

  2. Authorization Pop-up: Because we used setAuthorizationAction, Speech-to-Text Transcription Tool with Google Workspace opens a secure pop-up window pointing to the authorizationUrl.

  3. User Consent: The user sees the standard Google consent screen, where they grant permission for the scopes we requested.

  4. Redirection to Callback: After consent is granted, Google redirects the user’s browser to the redirect_uri we specified. This URI points to a special Apps Script endpoint.

  5. Callback Function Execution: Apps Script invokes the callback function we registered in our service configuration. In our blueprint, this is the authCallback function.

The authCallback function is the server-side handler that completes the OAuth2 dance. It receives the authorization code from Google, exchanges it for an access token and an ID token, and securely stores them.

Here’s what the authCallback(request) function looks like. It’s responsible for handling the redirect and finalizing the authentication.


/**

* The callback function that is invoked after the user completes the

* OAuth flow.

* @param {Object} request The request object from the callback.

* @returns {HtmlOutput} A simple HTML page to show in the pop-up.

*/

function authCallback(request) {

const oauthService = getOAuthService();

const isAuthorized = oauthService.handleCallback(request);

if (isAuthorized) {

// The user is now authenticated. We can fetch and store their info.

const idToken = oauthService.getIdToken();

const payload = JSON.parse(Utilities.newBlob(Utilities.base64Decode(idToken.split('.')[1])).getDataAsString());

// Store user email for personalization.

const userProperties = PropertiesService.getUserProperties();

userProperties.setProperty('user_email', payload.email);

return HtmlService.createHtmlOutput('Success! You can now close this tab.');

} else {

return HtmlService.createHtmlOutput('Access Denied. Please try again.');

}

}

The most critical line here is oauthService.handleCallback(request). This method, provided by the OAuth2 library, handles the complex token exchange process behind the scenes and securely stores the resulting tokens in PropertiesService for later use.

Displaying user state authenticated vs unauthenticated

An add-on should always reflect the user’s current authentication state. We need a central function that checks if the user has a valid, non-expired token and then decides which card to display.

The OAuth2 library makes this check trivial with the hasAccess() method. It automatically checks for a stored access token and even uses the refresh token to get a new one if the old one has expired.

Let’s modify our main entry point (e.g., onHomepage) to include this conditional logic.


/**

* The main function that builds the add-on's homepage card.

* This is the primary entry point when a user opens the add-on.

* @returns {Card} The card to display.

*/

function onHomepage(e) {

if (getOAuthService().hasAccess()) {

return createAuthenticatedCard();

} else {

return createLoginCard();

}

}

Now, we just need to create the createAuthenticatedCard(). This card should welcome the user and provide a way to sign out. Signing out is as simple as resetting the OAuth service, which clears the stored tokens.


/**

* Creates a card for an authenticated user, displaying a welcome message

* and a sign-out button.

* @returns {Card} The authenticated user card.

*/

function createAuthenticatedCard() {

// Retrieve the stored user email for a personalized message.

const email = PropertiesService.getUserProperties().getProperty('user_email');

// Create a sign-out action. This will call the signOut function.

const signOutAction = CardService.newAction()

.setFunctionName('signOut');

const welcomeSection = CardService.newCardSection()

.addWidget(CardService.newTextParagraph()

.setText(`Welcome, ${email}! You are successfully connected.`)

)

.addWidget(CardService.newTextButton()

.setText('Sign Out')

.setOnClickAction(signOutAction)

);

const card = CardService.newCardBuilder()

.setHeader(CardService.newCardHeader().setTitle('Firebase Auth Demo'))

.addSection(welcomeSection)

.build();

return card;

}

/**

* Signs the user out by clearing the stored OAuth tokens and refreshing the card.

* @returns {ActionResponse} An action response that rebuilds the UI.

*/

function signOut() {

getOAuthService().reset();

// Re-render the UI. Since hasAccess() will now be false,

// the login card will be shown.

const navigation = CardService.newNavigation().updateCard(onHomepage());

return CardService.newActionResponseBuilder()

.setNavigation(navigation)

.build();

}

With this logic in place, your add-on now has a complete, state-aware authentication UI. It securely guides users through the login process and provides a clear and persistent interface reflecting their status.

Step 4 Validating Tokens and Managing User Data

With the ID token now in our server-side Apps Script environment, the real security work begins. A token received from the client is merely a claim of identity; it’s our responsibility on the server to rigorously verify that claim before granting any access. This step is the cornerstone of the add-on’s security model, transforming an untrusted request into a verified, actionable session.

Server-side validation of Firebase ID tokens in Apps Script

Unlike server environments with official Firebase Admin SDKs, [AI Powered Cover Letter Automated Job Creation in Jobber from Gmail Engine](https://votuduc.com/AI-Powered-Cover-Letter-Automated Quote Generation and Delivery System for Jobber-Engine-p111092) requires a manual approach to validate JWTs. This process involves cryptographic verification against Google’s public keys to ensure the token is authentic and has not been tampered with.

Here’s the breakdown of the validation logic:

  1. Fetch Google’s Public Keys: Firebase ID tokens are signed using a private key. To verify this signature, we need the corresponding public key. Google publishes its current public keys at a specific endpoint. We must fetch these keys to proceed.

  2. Cache the Public Keys: Network requests are slow. Calling the public key endpoint for every single token validation is inefficient and unnecessary. The keys rotate periodically, but not that frequently. We’ll use Apps Script’s CacheService to cache the keys, respecting the Cache-Control header from the response.

  3. Decode and Verify the JWT: A JWT consists of a header, a payload, and a signature. The process is as follows:

  • Decode the token’s header to find the Key ID (kid), which tells us which public key to use for verification.

  • Using the matching public key, verify the token’s signature with Utilities.verifyRsaSha256Signature. This cryptographically proves the token was issued by your Firebase project.

  • Verify the claims within the payload to ensure the token is not expired (exp), was issued to your project (aud), and came from the correct issuer (iss).

Let’s implement this in a robust Apps Script function. This function will be our central gatekeeper for authentication.


/**

* Validates a Firebase ID token and returns the decoded payload if valid.

* Throws an error if the token is invalid.

* @param {string} token The Firebase ID token string.

* @return {object} The decoded token payload.

*/

function validateFirebaseToken(token) {

const FIREBASE_PROJECT_ID = 'YOUR_FIREBASE_PROJECT_ID'; // <-- Replace with your Project ID

try {

const parts = token.split('.');

if (parts.length !== 3) {

throw new Error('Invalid token structure');

}

const header = JSON.parse(Utilities.newBlob(Utilities.base64DecodeWebSafe(parts[0])).getDataAsString());

const payload = JSON.parse(Utilities.newBlob(Utilities.base64DecodeWebSafe(parts[1])).getDataAsString());

const signature = Utilities.base64DecodeWebSafe(parts[2]);

// Step 1 & 2: Get and cache Google's public keys

const publicKeys = getGooglePublicKeys_();

const key = publicKeys[header.kid];

if (!key) {

throw new Error('Token signed by an unknown key');

}

// Step 3: Verify the signature

const signedContent = parts[0] + '.' + parts[1];

const isValidSignature = Utilities.verifyRsaSha256Signature(signedContent, signature, key);

if (!isValidSignature) {

throw new Error('Invalid token signature');

}

// Step 4: Verify payload claims

const now = Math.floor(Date.now() / 1000);

if (payload.iat > now || payload.exp < now) {

throw new Error('Token is expired or not yet valid');

}

if (payload.aud !== FIREBASE_PROJECT_ID) {

throw new Error('Token audience is invalid');

}

if (payload.iss !== `https://securetoken.google.com/${FIREBASE_PROJECT_ID}`) {

throw new Error('Token issuer is invalid');

}

if (!payload.sub) {

throw new Error('Token subject (user ID) is missing');

}

// If all checks pass, return the payload

return payload;

} catch (e) {

console.error('Token validation failed: ' + e.message);

throw new Error('Authentication failed. Please sign in again.');

}

}

/**

* Fetches and caches Google's public keys for JWT signature verification.

* @return {object} An object mapping key IDs to PEM-formatted public keys.

* @private

*/

function getGooglePublicKeys_() {

const cache = CacheService.getScriptCache();

const cacheKey = 'firebase-public-keys';

const cachedKeys = cache.get(cacheKey);

if (cachedKeys) {

return JSON.parse(cachedKeys);

}

const response = UrlFetchApp.fetch('https://www.googleapis.com/robot/v1/metadata/x509/[email protected]');

const keys = JSON.parse(response.getContentText());

// Extract max-age from Cache-Control header to set cache duration

const cacheControl = response.getHeaders()['Cache-Control'];

const maxAge = /max-age=(\d+)/.exec(cacheControl);

const cacheDuration = maxAge ? parseInt(maxAge[1], 10) : 3600; // Default to 1 hour

cache.put(cacheKey, JSON.stringify(keys), cacheDuration);

return keys;

}

Associating the Firebase UID with user-scoped data

Once the token is validated, its payload gives us the most important piece of information: the Firebase User ID (uid), available in the sub (subject) claim. This UID is the canonical, immutable identifier for the user within your system. It’s far superior to using an email address, which can change.

To create a persistent session for the user within the add-on, we associate this Firebase UID with their Google account for this specific add-on. The perfect tool for this in Apps Script is the PropertiesService.

PropertiesService.getUserProperties() provides a key-value store that is scoped to:

  1. The current user.

  2. The current script.

This means the data is private to the user and your add-on, making it an ideal and secure place to store the user’s verified identity.

Here’s how you link the identity after a successful login:


/**

* Stores the verified Firebase UID in the user's private properties.

* This function would be called by your server-side login handler.

*

* @param {string} idToken The ID token received from the client.

*/

function establishUserSession(idToken) {

try {

const payload = validateFirebaseToken(idToken);

const firebaseUid = payload.sub;

const userProperties = PropertiesService.getUserProperties();

userProperties.setProperty('FIREBASE_UID', firebaseUid);

// You can also store other useful, non-sensitive info

userProperties.setProperty('FIREBASE_EMAIL', payload.email || '');

return { success: true, email: payload.email };

} catch (e) {

return { success: false, error: e.message };

}

}

Now, in any subsequent server-side function, you can quickly and easily retrieve the user’s identity without needing to re-validate a token on every call.

Best practices for securing data access based on verified identity

With the user’s Firebase UID securely stored in UserProperties, you can now enforce access control throughout your add-on’s backend logic. Every function that performs a sensitive action or handles user data must be protected.

1. Create an “Auth Guard” Function

Instead of repeating authentication checks in every function, create a single, reusable “guard” function. This function’s job is to verify that the current user has an active session and return their identity. It should be the very first line of any protected server-side function.


/**

* Auth Guard: Verifies the current user is authenticated and returns their UID.

* Throws an error if the user is not authenticated.

* @return {string} The authenticated user's Firebase UID.

*/

function getVerifiedUid_() {

const userProperties = PropertiesService.getUserProperties();

const uid = userProperties.getProperty('FIREBASE_UID');

if (!uid) {

// This error will be propagated back to the client, which should

// trigger a re-authentication flow.

throw new Error('Authorization required. Please sign in.');

}

return uid;

}

2. Protect Every Server-Side Function

Now, use this guard function to protect your business logic. This pattern ensures that no unauthenticated access can occur.


/**

* Example protected function: Fetches user-specific data from a spreadsheet.

* @return {object[]} An array of data rows belonging to the user.

*/

function getUserData() {

const uid = getVerifiedUid_(); // Auth guard is the first call

// Now, use the verified UID to scope all data access.

// Never trust any user identifier sent from the client.

const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('UserData');

const allData = sheet.getDataRange().getValues();

// Filter data to return only what belongs to this user

const userData = allData.filter(row => row[0] === uid); // Assuming UID is in the first column

return userData;

}

3. Implement a Secure Logout

A logout function is just as important as a login function. It must clear the server-side session state to fully de-authorize the user.


/**

* Clears the user's session by deleting their identity from UserProperties.

*/

function logoutUser() {

try {

const userProperties = PropertiesService.getUserProperties();

userProperties.deleteProperty('FIREBASE_UID');

userProperties.deleteProperty('FIREBASE_EMAIL');

return { success: true };

} catch (e) {

return { success: false, error: e.message };

}

}

By consistently applying this pattern—validating tokens, storing the UID in UserProperties, and using a guard function—you build a secure and reliable foundation for your Google Workspace Add-on. Every action is tied to a cryptographically verified identity, ensuring data integrity and user privacy.

Conclusion: Unlocking Scalable and Secure Add-on Architecture

We’ve journeyed through the process of architecting a Google Workspace Add-on that doesn’t just work, but is built to scale securely and efficiently. By treating Firebase Authentication as a distinct identity layer, we’ve moved beyond the default, tightly-coupled model and into a world of modern, flexible application design. This GDE blueprint isn’t just about security; it’s about future-proofing your entire ecosystem.

Recap: The benefits of an independent identity layer

Let’s distill the core advantages of this architectural pattern. Decoupling your add-on’s identity from the Google Workspace session token isn’t an extra step—it’s a strategic investment in your product’s future.

  • Platform Agnosticism: Your service is no longer just a “Google Workspace Add-on.” It’s a standalone application with its own user base. This architecture is your ticket to building a companion web app, a mobile client, or integrating with other platforms, all sharing a single, consistent source of truth for user identity.

  • Robust, Managed Security: You delegate the heavy lifting of authentication—password hashing, token issuance and verification, multi-factor authentication (MFA), and account recovery—to a dedicated, battle-tested service. This dramatically reduces your security surface area and lets you focus on your application’s core business logic, not on reinventing the identity wheel.

  • Enhanced User Experience & Flexibility: With Firebase Auth, you can offer a multitude of sign-in providers (Google, Microsoft, GitHub, Email/Password, etc.). This meets users where they are and can be crucial for adoption outside the pure-Google ecosystem. The user’s identity is tied to your service, not just their temporary Google session.

  • Scalable and Granular Authorization: This model is the foundation for sophisticated authorization. By controlling the identity layer, you can enrich user profiles with custom claims that define roles and permissions (e.g., admin, premium_user, viewer). This data, embedded directly in the ID token, enables you to enforce fine-grained access control across your entire backend infrastructure, from Cloud Functions to Firestore rules.

Next steps for advanced identity management

Mastering this foundational pattern opens the door to even more powerful identity and access management (IAM) strategies. As your application grows in complexity and user base, consider exploring these advanced topics:

  • Implement Role-Based Access Control (RBAC) with Custom Claims: Move beyond simple authentication. Use a backend process (like a Cloud Function triggered on user creation) to set custom claims on a user’s Firebase token. This allows you to define roles that your backend services can instantly verify without needing extra database lookups.

  • Graduate to Google Cloud Identity Platform (GCIP): For enterprise-grade needs, consider GCIP. It’s the big brother to Firebase Auth, offering multi-tenancy, support for enterprise identity protocols like SAML and OpenID Connect (OIDC), and advanced compliance features. The migration path from Firebase Auth is smooth, making this a natural evolution.

  • Refine Token & Session Management: Investigate advanced session management techniques. Implement token revocation for immediate sign-out capabilities, configure refresh token expiration to enforce periodic re-authentication for sensitive operations, and align session cookie policies with your security requirements.

  • Integrate with Service-to-Service Authentication: As your backend grows into a microservices architecture, use the principles of strong identity to secure communication between services. A service can use its own identity token (issued by Google Cloud IAM) to securely call another service, which can then verify the caller’s permissions before responding.

Ready to scale your architecture? Book a GDE discovery call

Theory and diagrams are one thing; implementing a robust, secure, and scalable architecture for your specific business case is another. The nuances of token exchange, error handling, and cloud infrastructure can be complex.

If you’re looking to apply this blueprint to your own project and want to ensure you’re building on a solid foundation, I’m here to help. Let’s translate this pattern into a concrete plan for your application.

Book a complimentary 30-minute GDE discovery call to discuss your unique challenges, validate your architectural approach, and accelerate your development journey.


Tags

Google WorkspaceFirebaseApps ScriptAuthenticationSecurityGDE

Share


Previous Article
Securing AI Agents in Apps Script with The Principle of Least Privilege
Vo Tu Duc

Vo Tu Duc

A Google Developer Expert, Google Cloud Innovator

Stop Doing Manual Work. Scale with AI.

Hi, I'm Vo Tu Duc (Danny), a recognised Google Developer Expert (GDE). I architect custom AI agents and Google Workspace solutions that help businesses eliminate chaos and save thousands of hours.

Want to turn these blog concepts into production-ready reality for your team?
Book a Discovery Call

Table Of Contents

1
The Identity Challenge Beyond Standard Workspace Triggers
2
Core Architecture: A Custom OAuth Flow for Your Add-on
3
Step 1: Setting Up Your Firebase and GCP Environment
4
Step 2 Implementing the Apps Script OAuth2 Service
5
Step 3 Building the User-Facing Authentication Card
6
Step 4 Validating Tokens and Managing User Data
7
Conclusion: Unlocking Scalable and Secure Add-on Architecture

Portfolios

AI Agentic Workflows
Cloud Engineering
AppSheet Solutions
Change Management
Strategy Playbooks
Product Showcase
Uncategorized
Workspace Automation

Related Posts

Mastering AI Change Management A GDEs Data Driven Playbook
May 22, 2026
© 2026, All Rights Reserved.
Powered By

Quick Links

Book a CallAbout MeVolunteer Legacy

Social Media