HomeAbout MeBook a Call

Architecting a Secure Firebase Proxy for Google Apps Script

By Vo Tu Duc
Published in Cloud Engineering
May 05, 2026
Architecting a Secure Firebase Proxy for Google Apps Script

Connecting your apps creates powerful workflows, but sharing that data securely is a major challenge. Discover a low-code method to safely expose your workspace information without creating vulnerabilities.

image 0

The Challenge: Exposing Workspace Data Securely

[AI Powered Cover Letter [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606) Engine](https://votuduc.com/AI-Powered-Cover-Letter-Automated Quote Generation and Delivery System for Jobber-Engine-p111092) is a powerful low-code platform that acts as the connective tissue for [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). It allows you to automate workflows, build custom add-ons, and integrate services with unparalleled access to user data in Docs, Sheets, and Drive. One of its most potent features is the ability to deploy a script as a web app, effectively turning your code into a live API endpoint. This opens up a world of possibilities for connecting Workspace data to external applications, dashboards, and services.

However, with great power comes great responsibility—and significant security challenges. Simply deploying a script as a public web app creates a direct, often unprotected, bridge into the heart of your organization’s data. This direct exposure introduces risks that range from inconvenient to catastrophic, creating a critical gap in security and compliance that must be addressed.

image 1

Why direct Apps Script web app exposure is a risk

When you deploy an Apps Script as a web app, you are given a unique .script.google.com URL that executes your doGet(e) or doPost(e) functions upon receiving HTTP requests. While simple and effective for internal tools, exposing this URL directly to the public internet or third-party services is fraught with peril for several reasons:

  • Brittle Authentication and Authorization: Apps Script’s permission model is blunt. You can typically set access to “Only myself,” “Anyone within [Your Domain],” or “Anyone.” To integrate with an external service, you’re often forced to use “Anyone,” which means the endpoint is completely public and unauthenticated. There is no built-in mechanism for handling standard API keys, OAuth tokens, or other fine-grained authorization strategies. This leaves your endpoint vulnerable to unauthorized access, enumeration, and abuse.

  • No Defense Against Abuse: A public endpoint is a target. Apps Script web apps have no native protection against denial-of-service (DoS) attacks or aggressive request patterns. A misconfigured external service or a malicious actor could easily bombard your script with requests, quickly exhausting your daily execution quotas and rendering your application useless for all users. There is no built-in rate-limiting, throttling, or IP-based blocking.

  • Lack of Observability: How do you know who is calling your endpoint? How often? Are the requests succeeding or failing? With a direct Apps Script web app, you are essentially flying blind. While you can implement basic logging to a spreadsheet or to Stackdriver, you lack the sophisticated monitoring, alerting, and request tracing that are standard for any production-grade API. This makes it nearly impossible to detect security anomalies, debug issues, or analyze usage patterns.

  • Direct Exposure of a Privileged Environment: An Apps Script project often runs with broad OAuth scopes, granting it extensive permissions to read and write data across AC2F Streamline Your Google Drive Workflow. A vulnerability in your web app’s code (e.g., an injection flaw or improper input handling) could be exploited by an attacker to leverage these powerful permissions, potentially leading to a significant data breach.

The compliance and security gap for external services

Beyond the immediate technical risks, exposing an Apps Script web app directly creates a significant gap in modern security and compliance standards. External systems and enterprise security teams expect a certain level of maturity from an API, and Apps Script’s raw endpoint simply doesn’t meet the bar.

This gap manifests in several ways:

  • Incompatible Security Models: Modern applications expect to authenticate with standard mechanisms like revocable API keys in headers, JWTs, or a full OAuth 2.0 flow. Developers using Apps Script are often forced to implement insecure workarounds, such as passing a shared secret as a query parameter—a practice that is a well-known security anti-pattern, as URLs are frequently logged in server logs, browser history, and network proxies.

  • Audit and Compliance Failures: For organizations subject to regulations like SOC 2, HIPAA, or GDPR, robust audit trails are not optional. You must be able to prove who accessed what data and when. A direct Apps Script endpoint provides no reliable audit log of incoming requests. Furthermore, you cannot enforce critical security controls like IP whitelisting or geofencing, making it difficult to satisfy compliance requirements for data residency and access control.

  • Secret Management Nightmare: To function, your Apps Script may need to call other APIs, requiring it to store secrets like API keys or database credentials. Storing these directly in Apps Script’s Properties Service is a common practice, but it couples your secrets directly to the script’s execution environment, which is itself publicly exposed.

Introducing the solution: A decoupled security proxy architecture

The fundamental problem is the tight coupling between the public-facing endpoint and the privileged code that accesses your data. The solution, therefore, is to decouple them. Instead of exposing Apps Script directly, we will place a dedicated, modern service layer in between the consumer and the script. This is the security proxy architecture.

In this model, the external world never communicates with your Apps Script web app directly. Instead, all requests are sent to a robust, secure proxy endpoint. This proxy becomes the gatekeeper, responsible for handling all the critical tasks that Apps Script cannot:

  • Modern Authentication & Authorization: The proxy manages API keys, validates JWTs, and handles other complex authentication schemes. It only forwards valid, authorized requests to the Apps Script.

  • Rate Limiting & Throttling: It protects your script from abuse by enforcing usage quotas and rejecting excessive traffic.

  • Request Validation & Transformation: It can inspect, clean, and even reshape incoming requests before they ever touch your Apps Script code, reducing the attack surface.

  • Centralized Logging & Monitoring: It provides a single point of observability, giving you detailed logs and metrics on every single request.

  • Secure Credential Management: The proxy securely manages the credentials needed to call the now-private Apps Script, abstracting this responsibility away from the end consumer.

By implementing this architecture, your Apps Script web app is relegated to its proper role: a private, trusted execution environment for business logic. The proxy, built on a platform designed for security and scale, handles the messy reality of the public internet. This transforms your simple script into a secure, manageable, and production-ready API.

Architectural Blueprint: The Firebase Security Layer

Before we dive into the code, let’s zoom out and look at the blueprint for our secure proxy. The core principle here is defense in depth. We aren’t relying on a single password or API key. Instead, we’re building a series of security gates, each with a specific job. A request from a client must successfully pass through every gate before it’s allowed to trigger our Apps Script. This layered approach dramatically reduces the attack surface and ensures that our script, which often has powerful permissions within our Automated Client Onboarding with Google Forms and Google Drive., remains protected.

High-level diagram of the request flow

To visualize how these layers interact, consider the following flow. Each step represents a distinct checkpoint in the request’s journey from the user’s browser to the Google Sheet and back.


sequenceDiagram

participant Client

participant API Gateway

participant Cloud Function (Proxy)

participant Firebase (Auth/Firestore)

participant Apps Script

Client->>+Firebase (Auth/Firestore): 1. Authenticate (e.g., Google Sign-In)

Firebase (Auth/Firestore)-->>-Client: 2. Receive Firebase ID Token (JWT)

Client->>+API Gateway: 3. Call API Endpoint with ID Token

API Gateway->>API Gateway: 4. Validate ID Token Signature & Expiry

Note over API Gateway: Gate 1: Is this a valid, known user?

API Gateway->>+Cloud Function (Proxy): 5. Forward validated request

Cloud Function (Proxy)->>+Firebase (Auth/Firestore): 6. Check permissions (e.g., read Firestore ACL)

Note over Cloud Function (Proxy), Firebase (Auth/Firestore): Gate 2: Is this user AUTHORIZED to do this?

Firebase (Auth/Firestore)-->>-Cloud Function (Proxy): 7. Return permission status

alt Permissions Granted

Cloud Function (Proxy)->>+Apps Script: 8. Invoke Web App with authorized payload

Apps Script->>Apps Script: 9. Execute G Suite logic (e.g., write to Sheet)

Apps Script-->>-Cloud Function (Proxy): 10. Return result

else Permissions Denied

Cloud Function (Proxy)-->>Cloud Function (Proxy): 8a. Reject request

end

Cloud Function (Proxy)-->>-API Gateway: 11. Forward final response

API Gateway-->>-Client: 12. Return response to client

This diagram illustrates our multi-stage security model. A request is first authenticated, then authorized, and only then is it executed. Let’s break down the role of each component.

Component 1: GCP API Gateway as the public-facing entrypoint

Think of the API Gateway as the bouncer at the front door of your club. It’s a hardened, scalable, and fully-managed Google Cloud service whose only job is to inspect incoming traffic before it ever reaches your application logic.

Its key responsibilities in our architecture are:

  • Public Endpoint: It provides a stable, professional URL for your application (e.g., api.yourdomain.com). This decouples your client-side code from the underlying, often ugly, Cloud Function or Apps Script URLs, which can change.

  • Initial Authentication: This is its most critical security function. We will configure the API Gateway with a security definition that requires a valid Firebase Auth ID Token (a JWT) in the Authorization header of every request. The gateway automatically validates the token’s signature against Google’s public keys, checks its expiration date, and ensures it was issued for your Firebase project. If any of these checks fail, the request is rejected with a 401 Unauthorized error immediately, without ever invoking our backend code or incurring costs.

  • Traffic Management: It handles essential but tedious tasks like Cross-Origin Resource Sharing (CORS) policies, rate limiting to prevent abuse, and routing requests to the correct backend service (in our case, the proxy Cloud Function).

By using API Gateway, we offload the first and most important line of defense to a dedicated, battle-tested Google service.

Component 2: Firebase for authentication and rule-based validation

If the API Gateway is the bouncer checking for a valid ID, Firebase is the system that issues the ID and maintains the VIP list. It handles two distinct but related concepts: who the user is (authentication) and what they are allowed to do (authorization).

  • Authentication: Firebase Authentication provides a suite of easy-to-implement sign-in methods (Google, email/password, social providers, etc.). When a user successfully signs in on the client-side, the Firebase SDK provides the all-important ID Token. This token is a cryptographically signed statement saying, “I, the Firebase project, certify that this request is coming from user with UID xyz123.” This is the token our API Gateway validates.

  • Rule-Based Validation (Authorization): This is the heart of our granular security model. Authentication tells us who the user is, but it doesn’t tell us what they’re allowed to do. We achieve this by using a Firebase database, like Firestore, as an Access Control List (ACL). Your proxy function, after receiving a validated request from the API Gateway, will perform a check against Firestore before proceeding.

For example, the function can query a permissions collection to see if the authenticated user’s UID has the necessary rights. A document at permissions/{userId} might look like this:


{

"email": "[email protected]",

"canGenerateInvoices": true,

"canDeleteRecords": false

}

Before calling the Apps Script function that generates an invoice, our proxy function first checks if canGenerateInvoices is true for the incoming user. This moves authorization logic out of your Apps Script and into a secure, easily manageable database, completely fulfilling the principle of least privilege.

Component 3: Apps Script as the sandboxed execution environment

Finally, we have Genesis Engine AI Powered Content to Video Production Pipeline. In this architecture, its role is that of a highly specialized, privileged worker. It is no longer a public-facing web server; it’s a backend system that only accepts requests from a single, trusted source: our proxy function.

Key characteristics of its role:

  • Sandboxed Execution: Apps Script runs in a secure Google environment. It can’t access the local file system or perform arbitrary network requests, which limits potential damage if the code were ever compromised.

  • Native G Suite Integration: This is its superpower. The script runs with a pre-configured identity (either the user running it or a service account) that has been granted specific OAuth scopes to interact with Sheets, Docs, Drive, Calendar, etc. It handles all the complexity of token management for Google APIs under the hood.

  • Business Logic, Not Security Logic: By placing it behind the proxy, we free the Apps Script code from concerning itself with user authentication, session management, or complex authorization rules. Its job is simplified: receive a validated, authorized instruction from the proxy and execute it. The script should be deployed as a Web App, but its permissions should be set to “Execute as me” and “Who has access: Anyone” (or restricted to your G Suite domain), because its URL is a secret known only to your proxy function, and all access control is handled by the preceding layers.

Implementation Step-by-Step Configuration

With the architecture defined, let’s dive into the hands-on configuration of each component. This section provides a detailed walkthrough, complete with code snippets and security best practices, to bring our secure proxy to life.

Setting up your Firebase project and Firestore

First, we need a foundation. This involves creating a Firebase project and enabling Firestore, which will act as our secure, asynchronous request queue.

  1. Create a Firebase Project: Navigate to the Firebase Console and click “Add project”. Follow the on-screen prompts. If you have an existing Google Cloud project you’d like to use, you can select it; otherwise, Firebase will create one for you.

  2. Enable Firestore: Once your project is ready, go to the “Build” section in the left-hand menu and click on “Firestore Database”.

  • Click “Create database”.

Choose* Native mode**. This is the latest and most feature-rich version of Firestore.

  • Select a location for your data. Choose a location that is geographically close to your users for lower latency.

For security rules, select* Start in test mode**. This allows open access for 30 days. Warning: We will immediately replace these permissive rules in the next step. Never leave a production database in test mode.

  1. Define the Data Structure: Our proxy will rely on a single Firestore collection, which we’ll call requests. When a client wants to execute a task, it will create a new document in this collection. To make our security rules simple and effective, we will use a client-generated, cryptographically secure unique ID as the document ID itself. This ID will serve as our single-use token.

A document in the /requests/{token} collection will look like this:


{

"payload": { "recipient": "[email protected]", "subject": "Hello!", "body": "..." },

"status": "pending",

"createdAt": "2023-10-27T10:00:00Z",

"result": null,

"processedAt": null

}

  • {token} (Document ID): A unique, single-use token generated by the client.

  • payload: An object containing the specific data your Apps Script needs to perform its task.

  • status: A state-management field (pending, processing, completed, error).

  • createdAt: A timestamp for when the request was submitted.

Crafting Firebase Security Rules for token and data validation

This is the heart of our security model. Firebase Security Rules are a non-negotiable step to prevent abuse. Our rules will enforce three critical constraints:

  1. Only new documents can be created. No one can read, update, or delete them directly.

  2. A document can only be created if its ID (the token) has never been used before, preventing replay attacks.

  3. The data being written must conform to a specific structure.

In the Firebase Console, navigate to Firestore Database > Rules. Replace the default test rules with the following:


rules_version = '2';

service cloud.firestore {

match /databases/{database}/documents {

// Match any document in the 'requests' collection, where {token} is a wildcard.

match /requests/{token} {

// Allow a client to create a request document only if all conditions are met.

allow create: if

// 1. Ensure the token (document ID) hasn't been used before.

!exists(/databases/$(database)/documents/requests/$(token))

&&

// 2. Validate the structure of the incoming data.

isValidRequestData(request.resource.data);

// 3. Deny all other operations from the client.

// Only our backend with admin privileges can read or update these documents.

allow read, update, delete: if false;

}

// A helper function to enforce the data schema for new requests.

function isValidRequestData(data) {

return

// The document must contain exactly these three fields.

data.keys().hasAll(['payload', 'status', 'createdAt']) && data.keys().size() == 3

&&

// Validate the data types and values.

data.payload is map

&&

data.status == 'pending'

&&

data.createdAt == request.time; // Enforce server-side timestamp.

}

}

}

Breakdown of the Rules:

  • match /requests/{token}: This rule applies to any document within the requests collection.

  • !exists(...): This is our anti-replay mechanism. Firestore checks if a document with the same ID (token) already exists. If it does, the write is rejected.

  • isValidRequestData(): This function acts as a schema validator. It ensures the client sends exactly the fields we expect (payload, status, createdAt) with the correct initial values.

  • data.createdAt == request.time: This clever rule forces the client to use FieldValue.serverTimestamp() for the createdAt field, ensuring we have a trustworthy, server-generated timestamp.

  • allow read, update, delete: if false;: This locks down the collection completely. Clients can only submit new jobs. They cannot see other jobs, and they cannot modify their job once submitted. The status will be updated by our trusted Apps Script backend, which uses admin credentials that bypass these client-facing rules.

Deploying the Apps Script web app as a service endpoint

Now we create the worker: a [Architecting Multi Tenant AI Workflows in Building Modular Agentic Apps Script with Gemini Function Calling](https://votuduc.com/architecting-multi-tenant-ai-workflows-in-google-apps-script-p-20260321290501) that will be invoked to process jobs from the Firestore queue. This script will be deployed as a Web App, providing a unique URL that acts as our service endpoint.

  1. Create a New Apps Script Project: Go to script.google.com and start a new project.

  2. Add the FirebaseApp Library: This library simplifies communication with Firebase services.

  • Click the + icon next to “Libraries”.

  • Enter the following script ID: 1hguuh4Zx72XVC1Zldm_vTtcUqaWv9J_b1doTC_j_dh_5EwR_u_MGO_s6

  • Select the latest version and use FirebaseApp as the identifier.

  1. Write the Web App Code (Code.gs):

The doPost(e) function is a special function in Apps Script that runs whenever the web app URL receives an HTTP POST request. Our endpoint will be called by a secure trigger (like a Cloud Function, which in turn is triggered by a Firestore document creation). This trigger will pass the document details to our script.


// This secret should match the one configured in your calling service (e.g., API Gateway or Cloud Function).

const AUTH_TOKEN = "YOUR_SUPER_SECRET_AUTHENTICATION_TOKEN";

// doPost is the entry point for the Web App

function doPost(e) {

// 1. Authenticate the request

const authToken = e.parameter.token;

if (authToken !== AUTH_TOKEN) {

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Unauthorized" }))

.setMimeType(ContentService.MimeType.JSON)

.setStatusCode(401);

}

// 2. Parse the incoming payload

const requestBody = JSON.parse(e.postData.contents);

const docPath = requestBody.docPath; // e.g., "requests/some-unique-token"

if (!docPath) {

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Missing docPath" }))

.setMimeType(ContentService.MimeType.JSON)

.setStatusCode(400);

}

try {

// 3. Connect to Firestore using a service account

const firestore = getFirestoreInstance();

// 4. Mark the job as 'processing' to prevent redundant executions

firestore.updateDocument(docPath, { status: 'processing', processedAt: new Date() });

// 5. Fetch the full payload and execute business logic

const requestDoc = firestore.getDocument(docPath);

const payload = requestDoc.fields.payload;

// --- YOUR BUSINESS LOGIC GOES HERE ---

// Example: Send an email using the payload data

MailApp.sendEmail(payload.recipient, payload.subject, payload.body);

const result = { status: "ok", message: "Email sent successfully." };

// -----------------------------------------

// 6. Write the final result back to Firestore

firestore.updateDocument(docPath, { status: 'completed', result: result });

return ContentService.createTextOutput(JSON.stringify({ status: "success" }))

.setMimeType(ContentService.MimeType.JSON);

} catch (error) {

// 7. Handle errors and log them back to Firestore

if (docPath) {

const firestore = getFirestoreInstance();

firestore.updateDocument(docPath, { status: 'error', result: { message: error.message, stack: error.stack } });

}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: error.message }))

.setMimeType(ContentService.MimeType.JSON)

.setStatusCode(500);

}

}

// Helper function to initialize Firestore connection

function getFirestoreInstance() {

// NOTE: Store these values securely using PropertiesService

const serviceAccountCredentials = {

"type": "service_account",

"project_id": "YOUR_FIREBASE_PROJECT_ID",

"private_key_id": "...",

"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",

"client_email": "[email protected]",

"client_id": "...",

// ... (rest of your service account JSON)

};

return FirebaseApp.getFirestore(serviceAccountCredentials.client_email, serviceAccountCredentials.private_key, serviceAccountCredentials.project_id);

}

  • Important: You must get your service account credentials (a JSON file) from Google Cloud Console > IAM & Admin > Service Accounts. Create a service account with “Cloud Datastore User” role, generate a JSON key, and copy its contents into the getFirestoreInstance function. For production, store these credentials securely using Apps Script’s PropertiesService.
  1. Deploy the Web App:

Click* Deploy > New deployment**.

Select* Web app** as the deployment type.

  • In the configuration:

  • Execute as: Me (The script will run with your permissions).

  • Who has access: Anyone. This sounds insecure, but it’s not. Our AUTH_TOKEN check and the API Gateway we’ll configure next are the real security layers. This setting is required for an external service to be able to invoke the URL.

Click* Deploy**. Copy the generated Web app URL. You will need it in the next step.

Configuring API Gateway to route and secure requests

The final piece is the API Gateway. It acts as a professional, secure front door for our Apps Script web app. It hides the raw script.google.com URL, enforces API key usage, and provides a stable endpoint for our backend trigger mechanism (e.g., a Cloud Function).

  1. Enable API Gateway: In the Google Cloud Console, navigate to the API Gateway page and enable the API if it’s not already.

  2. Create an OpenAPI Specification: This YAML file defines the structure of your API. Create a file named openapi-spec.yaml.


swagger: "2.0"

info:

title: "Apps Script Proxy API"

description: "Securely proxies requests to a Google Apps Script web app."

version: "1.0.0"

schemes:

- "https"

produces:

- "application/json"

paths:

/process-request:

post:

summary: "Processes a request from the Firestore queue."

operationId: "processRequest"

x-google-backend:

address: "PASTE_YOUR_APPS_SCRIPT_WEB_APP_URL_HERE?token=YOUR_SUPER_SECRET_AUTHENTICATION_TOKEN"

security:

- api_key: []

responses:

"200":

description: "Success"

"401":

description: "Unauthorized"

"500":

description: "Internal Server Error"

securityDefinitions:

api_key:

type: "apiKey"

name: "key"

in: "query"

  • Replace PASTE_YOUR_APPS_SCRIPT_WEB_APP_URL_HERE with the URL you copied from the Apps Script deployment.

  • Replace YOUR_SUPER_SECRET_AUTHENTICATION_TOKEN with the exact same secret you defined in your Code.gs file. This securely passes the token from the gateway to the script.

  • The securityDefinitions section specifies that API consumers must provide an API key as a query parameter named key.

  1. Create an API Config:

In the Cloud Console, go to* API Gateway > APIs**.

Click* Create API**.

  • Upload your openapi-spec.yaml file.

Give your API a display name (e.g., apps-script-proxy-api) and click* Create API**.

  1. Create a Gateway:

Go to the* Gateways tab and click Create Gateway**.

  • Select the API Config you just created.

  • Give the gateway a display name and select your desired region.

Click* Create Gateway**. After a few minutes, the gateway will be deployed and you will get a public URL for it (e.g., https://my-gateway-id.uc.gateway.dev).

  1. Create and Restrict an API Key:

Navigate to* APIs & Services > Credentials**.

Click* Create Credentials > API key**.

  • Copy the generated key. This is what your calling service will use.

Click* Edit API key**. Under “API restrictions”, select “Restrict key”, choose the “Apps Script Proxy API” you created, and save.

Your secure endpoint is now live. The URL provided by the gateway is the stable, secure target that your backend trigger (e.g., a Cloud Function listening to new documents in the requests collection) will call, passing its API key for authentication.

Code Deep Dive: The Proxy Logic

With our architecture defined, it’s time to roll up our sleeves and translate theory into code. The heart of our secure proxy is a single Google Apps Script doPost(e) function. This function acts as the gatekeeper, meticulously inspecting every incoming request before deciding whether to grant it passage to the powerful Automated Discount Code Management System APIs that lie beyond.

Let’s break down the four critical stages of this process: token validation, API interaction, response structuring, and error handling.

Validating Firebase Auth ID tokens within Apps Script

This is the security cornerstone of our entire proxy. Any request that arrives without a valid, verifiable Firebase Auth ID token must be rejected outright. We can’t trust the request’s payload or its intentions until we’ve confirmed the identity of the sender.

Since Google Apps Script doesn’t have a built-in JWT (JSON Web Token) validation library, we’ll leverage a fantastic open-source one.

  1. Add the JWT Library: In your Apps Script project, go to Project Settings > Scrip Properties and add a property with the key FIREBASE_PROJECT_ID and your Firebase project ID as the value. Then, go to Libraries and add the following script ID for the JWT library: 1B7FSrk57_JpToB8M1i-p4V_adCx3SJsrMvE1Pcv_G234M_p_9c2at1E_. Use the latest version.

  2. The Validation Logic: Our validation function will perform several non-negotiable checks:

  • Fetch Google’s public keys to verify the token’s signature.

  • Decode the token.

  • Verify the aud (audience) claim matches our Firebase Project ID.

  • Verify the iss (issuer) claim points to our Firebase project.

  • Ensure the token hasn’t expired (exp claim).

Here’s a robust function to handle this. Place this in its own .gs file (e.g., Auth.gs) for better organization.


/**

* Validates a Firebase Auth ID token.

*

* @param {string} idToken The Firebase ID token to validate.

* @returns {object|null} The decoded token payload if valid, otherwise null.

*/

function validateFirebaseToken(idToken) {

try {

const FIREBASE_PROJECT_ID = PropertiesService.getScriptProperties().getProperty('FIREBASE_PROJECT_ID');

if (!FIREBASE_PROJECT_ID) {

console.error('FIREBASE_PROJECT_ID script property not set.');

return null;

}

// 1. Fetch Google's public keys for signature verification

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

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

// 2. Decode and verify the token using the library

const decodedToken = JWT.decode(idToken, {

certs: publicKeys,

audience: FIREBASE_PROJECT_ID,

issuer: `https://securetoken.google.com/${FIREBASE_PROJECT_ID}`

});

// 3. If we get here, the token is valid

console.log(`Successfully validated token for user UID: ${decodedToken.payload.user_id}`);

return decodedToken.payload;

} catch (e) {

console.error(`Token validation failed: ${e.toString()}`);

return null;

}

}

Passing validated requests to Automated Email Journey with Google Sheets and Google Analytics APIs

Once a request’s token has been validated, the proxy can proceed with its core task: executing an action against a Automated Google Slides Generation with Text Replacement API. The script now operates with confidence, knowing the request originated from an authenticated user of your Firebase application.

The validated token payload contains the user’s unique Firebase UID (user_id or uid), which is invaluable for logging and auditing.

Let’s implement the main doPost(e) function. It will extract the token, call our validation function, and if successful, perform a simple action like creating a Google Calendar event based on the request’s JSON payload.


// In your main Code.gs file

/**

* Main entry point for POST requests to the web app.

*

* @param {object} e The event parameter for a POST request.

* @returns {ContentService.TextOutput} A JSON response.

*/

function doPost(e) {

// --- AUTHENTICATION ---

const authToken = e.parameters.authorization;

if (!authToken) {

return createJsonResponse({ success: false, error: 'Authorization token missing.' }, 401);

}

const idToken = authToken.replace('Bearer ', '');

const validatedPayload = validateFirebaseToken(idToken);

if (!validatedPayload) {

// Log the failed attempt (more on this later)

logAccessAttempt(null, 'createCalendarEvent', 'Failed: Invalid Token');

return createJsonResponse({ success: false, error: 'Invalid authentication token.' }, 403);

}

// --- AUTHORIZATION & ACTION ---

try {

const requestData = JSON.parse(e.postData.contents);

// Example: Create a Google Calendar event

// In a real app, you'd get the calendar ID from a secure source.

const calendarId = '[email protected]';

const calendar = CalendarApp.getCalendarById(calendarId);

if (!calendar) {

throw new Error(`Calendar with ID ${calendarId} not found or inaccessible.`);

}

const event = calendar.createEvent(

requestData.title,

new Date(requestData.startTime),

new Date(requestData.endTime),

{ description: requestData.description }

);

// Log the successful attempt

logAccessAttempt(validatedPayload.user_id, 'createCalendarEvent', 'Success');

// --- RESPONSE ---

// Return a minimal, secure response

const responseData = {

eventId: event.getId(),

message: 'Event created successfully.'

};

return createJsonResponse({ success: true, data: responseData }, 200);

} catch (error) {

// Handle errors (more on this later)

return handleProxyError(error, validatedPayload.user_id, 'createCalendarEvent');

}

}

Structuring secure and minimal data responses

A common mistake is to return the raw, verbose object from a Automated Order Processing Wordpress to Gmail to Google Sheets to Jobber API call directly to the client. This can leak sensitive information like attendee email addresses, internal resource details, or private meeting links.

The principle of least privilege applies to data just as much as it does to permissions. Your proxy should act as a filter, returning only the bare minimum of information the client application needs to function.

We’ll create a helper function to standardize our JSON output. This ensures consistency for both success and error scenarios.


/**

* Creates a structured JSON response.

*

* @param {object} payload The data to be stringified.

* @param {number} statusCode The HTTP status code for the response.

* @returns {ContentService.TextOutput} The JSON response object.

*/

function createJsonResponse(payload, statusCode) {

const jsonString = JSON.stringify(payload);

return ContentService.createTextOutput(jsonString)

.setMimeType(ContentService.MimeType.JSON)

.setStatusCode(statusCode);

}

Notice in our doPost example, we didn’t return the entire CalendarEvent object. Instead, we constructed a new, minimal object:


const responseData = {

eventId: event.getId(),

message: 'Event created successfully.'

};

This gives the client exactly what it needs—the ID of the newly created event for future reference—and nothing more.

Handling errors and logging access attempts

Things will go wrong. Tokens will be invalid, payloads will be malformed, and upstream APIs will fail. A production-ready proxy must handle these failures gracefully and, most importantly, log them for security auditing and debugging.

Error Handling:

Our doPost function uses a try...catch block. The catch block is our safety net. It should prevent raw, revealing error messages from leaking to the client. Instead, it logs the detailed error internally and returns a generic, safe message to the user.


/**

* Handles errors, logs them, and returns a standardized error response.

*

* @param {Error} error The error object that was caught.

* @param {string} uid The Firebase UID of the user making the request.

* @param {string} action The action being attempted (e.g., 'createCalendarEvent').

* @returns {ContentService.TextOutput} A JSON error response.

*/

function handleProxyError(error, uid, action) {

console.error(`Error during action '${action}' for user '${uid}': ${error.toString()}`);

// Log the detailed error for internal review

logAccessAttempt(uid, action, `Failed: ${error.message}`);

// Return a generic error to the client

return createJsonResponse({

success: false,

error: 'An internal server error occurred. Please try again later.'

}, 500);

}

Access Logging:

Logging is not optional; it’s a core security feature. It allows you to trace who did what, and when. While Logger.log() or console.log() are fine for development, a more persistent solution like a dedicated Google Sheet is better for production.


/**

* Logs an access attempt to a designated Google Sheet.

*

* @param {string|null} uid The Firebase UID of the user.

* @param {string} action The action performed.

* @param {string} status The result of the action (e.g., 'Success', 'Failed: Invalid Token').

*/

function logAccessAttempt(uid, action, status) {

try {

// In Project Settings > Script Properties, add LOG_SHEET_ID

const LOG_SHEET_ID = PropertiesService.getScriptProperties().getProperty('LOG_SHEET_ID');

if (!LOG_SHEET_ID) return; // Fail silently if logging is not configured

const sheet = SpreadsheetApp.openById(LOG_SHEET_ID).getSheetByName('AccessLogs');

sheet.appendRow([

new Date(), // Timestamp

uid || 'N/A',

action,

status

]);

} catch (e) {

// Log to default logger if sheet logging fails

console.error(`Failed to write to audit log sheet: ${e.toString()}`);

console.log(`[AUDIT LOG] Timestamp: ${new Date()}, UID: ${uid}, Action: ${action}, Status: ${status}`);

}

}

By combining these four pillars—strict validation, controlled API execution, minimal responses, and robust logging—we transform a simple script into a secure and reliable proxy.

Advanced Security and Operational Considerations

Deploying the proxy is a significant milestone, but the journey towards a truly robust and secure architecture doesn’t end there. Production systems require ongoing vigilance. This section delves into the operational practices and advanced security measures that transform your proxy from a functional component into a resilient, observable, and cost-effective system.

Implementing robust logging and monitoring with Google Cloud’s Operations Suite

In a serverless architecture, you don’t have a server to SSH into. Your visibility into the system’s health and security posture comes entirely from logs and metrics. Flying blind is not an option; you must establish a comprehensive observability strategy from day one. Fortunately, Cloud Functions seamlessly integrates with Google Cloud’s Operations Suite (formerly Stackdriver).

Structured Logging in Cloud Functions

While a simple console.log("Function executed") works, it provides minimal value. The real power comes from structured logging, where you log JSON objects. This makes your logs machine-readable, allowing for powerful filtering, analysis, and alerting in Cloud Logging.

Consider this best-practice approach within your proxy function:


// Example of structured logging in the Cloud Function

exports.firebaseProxy = functions.https.onCall((data, context) => {

// Ensure the user is authenticated.

if (!context.auth) {

console.error(

"Unauthorized access attempt",

{

auth: null,

ip: context.rawRequest.ip,

action: data.action

}

);

throw new functions.https.HttpsError('unauthenticated', 'The function must be called while authenticated.');

}

const uid = context.auth.uid;

const action = data.action;

// ... your proxy logic here ...

// Log successful execution

console.log(

"Proxy action successful",

{

auth: { uid: uid },

action: action,

params: data.params, // Be careful not to log sensitive data

status: "SUCCESS"

}

);

return { result: "Action completed" };

});

By logging a JSON payload, you can now easily query for all logs where jsonPayload.status is “FAILURE” or filter all actions performed by a specific jsonPayload.auth.uid.

Monitoring and Alerting

Cloud Monitoring automatically collects key metrics from your Cloud Function, such as:

  • Invocation Count: The number of times your function is executed.

  • Execution Time: The duration of each execution, useful for spotting performance degradation.

  • Error Count: Both successful and failed executions are tracked.

You can go a step further by creating log-based metrics. For instance, you can create a metric that counts every time your structured log contains the message “Unauthorized access attempt”.

With these metrics in place, you can configure Alerting Policies. A critical alert to set up immediately is one that notifies you of an unusual spike in errors. For example: “Send an email to the security team if the ratio of 5xx errors to total invocations for firebaseProxy exceeds 2% over a 10-minute window.” This proactive alerting allows you to detect and respond to issues—be it a bug or a malicious attack—before your users even notice.

Strategies for rate limiting and preventing abuse

Your proxy is an entry point to your backend. If left unprotected, a buggy script or a malicious actor could call it in a tight loop, leading to a denial-of-service (DoS) condition and a massive bill. Rate limiting is an essential defense.

1. Concurrency Control (The Broad Sword)

The simplest form of protection is built directly into Cloud Functions. In your function’s deployment configuration, you can set the maxInstances property.


# gcloud command to deploy with max instances

gcloud functions deploy firebaseProxy \

--runtime nodejs18 \

--trigger-http \

--entry-point firebaseProxy \

--max-instances 10

This setting limits the number of concurrent function instances that can be running at any given time. If a burst of 100 requests comes in, only 10 will be processed simultaneously, with the rest being queued or eventually dropped. This is an effective, albeit blunt, tool for preventing catastrophic cost overruns and protecting downstream services (like your database) from being overwhelmed.

2. Granular, User-Based Rate Limiting (The Scalpel)

For more sophisticated control, you can implement per-user rate limiting using Firestore. This strategy involves creating a mechanism to track the number of requests a specific user makes within a given time window.

The Pattern:

  1. Data Model: In Firestore, create a collection, e.g., rateLimits. Each document ID is a user’s UID. The document contains a timestamp of their last request and a request count.

  2. Function Logic: When your proxy function is invoked:

  • Start a Firestore transaction.

  • Read the user’s document from the rateLimits collection.

  • Check the timestamp and count. If the user has exceeded their quota (e.g., >100 requests in the last minute), abort the transaction and throw a resource-exhausted (HTTP 429) error.

  • If they are within limits, increment their request count and update the timestamp within the same transaction.

  • Proceed with the main function logic.

This approach adds a small amount of latency and cost (one Firestore read and one write per invocation) but gives you precise control to prevent a single user from monopolizing the system’s resources.

Auditing and maintaining the security ruleset

Firebase Security Rules are not “set it and forget it.” They are a critical part of your application’s code and must be treated as such. As your application evolves, your rules must evolve with it. Neglecting them is a common path to introducing severe security vulnerabilities.

Establish a Regular Audit Cadence

Schedule a recurring security rule review (e.g., quarterly). During this review, you and your team should critically examine your firestore.rules or database.rules file, looking for:

  • Overly Broad Permissions: Search for any instances of .read: true or .write: true that could be tightened.

  • Business Logic Mismatches: Do the rules still accurately reflect your current business logic? A feature that was deprecated might leave behind a rule that is now a security hole.

  • **Insufficient Data Validation: Are you validating not just who can write data, but also what data they can write? Use request.resource.data to validate data types, field presence, and value ranges. For example: newData.age > 0 && newData.age is number.

Automate Auditing with the Emulator Suite

The most effective way to maintain your rules is to write automated tests for them. The Firebase Emulator Suite allows you to run a local instance of Firestore and test your security rules against a battery of simulated requests.

Using a testing framework like Mocha or Jest, you can write tests that assert expected outcomes:


// Example security rule test using the emulator

import { assertFails, assertSucceeds, initializeTestEnvironment } from "@firebase/rules-unit-testing";

describe("User profile security rules", () => {

it("should allow a user to write to their own profile", async () => {

const testEnv = await initializeTestEnvironment({ projectId: "my-test-project" });

const aliceContext = testEnv.authenticatedContext("alice");

const aliceDb = aliceContext.firestore();

// This write should be allowed by the rules

await assertSucceeds(aliceDb.collection("users").doc("alice").set({ name: "Alice" }));

await testEnv.cleanup();

});

it("should NOT allow a user to write to another user's profile", async () => {

const testEnv = await initializeTestEnvironment({ projectId: "my-test-project" });

const aliceContext = testEnv.authenticatedContext("alice");

const aliceDb = aliceContext.firestore();

// This write should be blocked by the rules

await assertFails(aliceDb.collection("users").doc("bob").set({ name: "Malicious" }));

await testEnv.cleanup();

});

});

By integrating these tests into a CI/CD pipeline, you can automatically prevent any change to your security rules that introduces a regression or vulnerability.

Cost analysis of the serverless components

One of the most attractive aspects of this serverless architecture is its cost-effectiveness, especially at low to medium scale. However, it’s crucial to understand the pricing models to avoid surprises.

| Service | Pricing Model | Generous Free Tier? | Key Cost Driver |

| :--- | :--- | :--- | :--- |

| Cloud Functions | Invocations, GB-seconds, Egress | Yes | High traffic volume, long-running functions |

| Firestore | Reads, Writes, Deletes, Storage | Yes | User-based rate limiting, frequent data access |

| Firebase Auth | Monthly Active Users (MAUs) | Yes | Very high user count (>50k/month) |

| Cloud Logging | Data Ingested & Stored | Yes | Excessive or verbose logging |

The Free Tier Reality

For many internal applications or projects with moderate traffic, this entire proxy architecture may operate entirely within the generous free tiers offered by Google Cloud and Firebase. As of this writing, this includes:

  • Cloud Functions: 2 million invocations/month

  • Firestore: 50,000 reads/day, 20,000 writes/day, and 1 GiB of storage

  • Firebase Authentication: First 50,000 MAUs are free

Controlling Costs Proactively

  1. Use the Google Cloud Pricing Calculator: Before launch, model your expected traffic (e.g., 100 users, each triggering the proxy 50 times a day) to get a rough cost estimate.

  2. Set Up Budget Alerts: This is non-negotiable for any production system. In the Google Cloud Console, create a budget for your project. Configure alert rules to send notifications when your spending reaches 50%, 90%, and 100% of your budget. This is your single most important safety net against runaway costs.

  3. Architect for Efficiency: Your architectural choices have direct cost implications. The granular rate-limiting strategy using Firestore, for example, adds two database operations to every single function call. While excellent for security, you must weigh that benefit against the added cost at scale. Always choose the right tool for the job.

Conclusion: Scaling Your Secure Architecture

We’ve journeyed through the design and implementation of a robust, secure proxy layer for Google Apps Script using Firebase and Google Cloud Functions. This architectural pattern is more than a clever workaround for platform limitations; it represents a strategic evolution in how we build enterprise-grade solutions on the Automated Payment Transaction Ledger with Google Sheets and PayPal platform. By decoupling our business logic from the Apps Script environment, we unlock a new tier of security, control, and performance that is essential for modern applications.

Recap of benefits: granular control, security, and scalability

Let’s distill the core advantages of adopting this proxy architecture. The benefits are not incremental; they are transformative.

  • Granular Control: You move beyond the blunt instrument of Apps Script’s all-or-nothing OAuth scopes. The Cloud Function becomes your central control plane, allowing you to implement fine-grained, context-aware business rules. You can validate payloads, enforce specific permissions based on the authenticated user’s identity (via Firebase Auth tokens), and implement sophisticated rate-limiting or request throttling—all completely independent of the Apps Script execution environment.

  • Enhanced Security: This pattern is a direct implementation of the principle of least privilege. Your Apps Script code no longer requires overly permissive scopes to powerful APIs. Instead, it holds a single, limited permission: the ability to invoke a specific, authenticated Cloud Function. The proxy function, secured by Google Cloud’s robust IAM (Identity and Access Management) framework, is the only component with the credentials to interact with sensitive backend services like Firestore. This dramatically reduces your application’s attack surface.

  • Massive Scalability: You are no longer constrained by the inherent quotas and performance ceilings of the Apps Script platform. Google Cloud Functions are designed for hyperscale, automatically scaling to handle thousands of concurrent requests. This means your solution can grow from a simple departmental tool to a mission-critical enterprise application without requiring a fundamental rewrite. By offloading heavy processing, complex computations, and intensive I/O to the serverless backend, your Apps Script remains lean, fast, and focused on its core task: user interaction within Google Docs to Web.

Extending the pattern to other Google Cloud services

The true power of this architecture lies in its versatility. While we focused on Firebase (Firestore and Auth), the proxy pattern is a universal blueprint for securely connecting Apps Script to the entire Google Cloud ecosystem.

Consider these possibilities:

  • Cloud Storage: Instead of granting Apps Script broad storage permissions, your proxy function can generate time-limited, signed URLs for specific file uploads or downloads. The Apps Script code interacts with these secure, ephemeral links, never touching the service account keys.

  • BigQuery: Allow users to run complex, parameterized queries from a Google Sheet without ever exposing BigQuery credentials or granting bigquery.jobs.create permissions to the Apps Script project. The proxy receives the query parameters, validates them, and executes the job using its own secure identity.

  • Secret Manager: Need to use a third-party API key within your script? Rather than hardcoding it or using insecure PropertiesService, the proxy function can fetch the specific secret from Secret Manager at runtime and provide it to the script, ensuring your most sensitive credentials are never exposed in the client-side or Apps Script code.

  • [Building Self Correcting Agentic Workflows with Building Self-Correcting Agentic Workflows with Vertex AI](https://votuduc.com/building-self-correcting-agentic-workflows-with-vertex-ai-p-20260321542526): Integrate powerful machine learning models directly into your Google Docs or Sheets. The Apps Script can send data (e.g., text from a document) to your proxy, which then handles the authentication and request formatting for the Vertex AI API, returning the inference result.

In every case, the Cloud Function acts as a trusted, secure, and scalable intermediary, enforcing your business logic and security posture.

Next Steps: Book a GDE discovery call for your enterprise needs

Implementing this architecture can introduce new considerations, especially within a complex enterprise environment concerned with compliance, advanced security policies, and integration with existing infrastructure. You might be asking:

  • How does this pattern fit into our CI/CD pipeline?

  • How can we integrate this with our existing Identity Provider (IdP) like Okta or Azure AD?

  • What are the best practices for logging, monitoring, and alerting for this architecture?

  • How do we manage different environments (dev, staging, prod) effectively?

If you’re looking to apply these principles to solve critical business challenges and need expert guidance to navigate the complexities of enterprise-scale implementation, let’s talk. As a Google Developer Expert (GDE) in Google Cloud and Workspace, I specialize in designing and building these types of secure, scalable solutions.

Book your complimentary 30-minute discovery call here to discuss your specific use case, architectural challenges, and how we can accelerate your project with proven best practices.


Tags

FirebaseGoogle Apps ScriptSecurityAPIGoogle WorkspaceProxy

Share


Previous Article
Architecting a Source of Truth Agent for Google Docs and Sheets
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

Portfolios

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

Related Posts

Automating GCP Security and Cost Audits with Gemini
May 22, 2026
© 2026, All Rights Reserved.
Powered By

Quick Links

Book a CallAbout MeVolunteer Legacy

Social Media