iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📑

A Practical Guide to Google OAuth Authentication with GitHub Pages and Google Apps Script

に公開

Introduction

In this article, I will explain a configuration where Google Apps Script (GAS) is used as an authentication gate and API for a Vue SPA delivered via GitHub Pages (gh-pages), specifically focusing on authentication.

This targets cases such as:

  • Wanting to publish without a dedicated server
  • Wanting to control access using Google Accounts
  • Wanting to strictly verify frontend tokens on the GAS side

The key point of the implementation is passing the id_token obtained on the frontend to GAS and verifying iss, aud, exp, email_verified, and the allowed email list on the GAS side.


Technologies Used

  • Google Cloud Console (OAuth Client creation)
  • Google Identity Services (GIS): Used to sign in with Google in the browser and obtain the id_token
  • Google Apps Script (Web App): Handles id_token verification and authorization checks
  • GitHub Pages: For frontend delivery
  • Vue 3 + Pinia: For managing login state and API communication

Technical Points (Focused on Authentication)

1. Solidify Google OAuth Setup First

First, create an OAuth client in the Google Cloud Console.

Steps

  1. Create a project in Google Cloud
  2. Set up the OAuth consent screen (enter required information)
  3. Create an OAuth 2.0 Client ID (Web Application)
  4. Configure the Client ID on the frontend side

In this project, it is loaded via frontend environment variables.

# .env.local
VITE_GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com

In App.vue, this value is referenced, and a warning is displayed when the login button is shown if it's not set.

// src/App.vue
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID || "";
const hasGoogleClientId = computed(() => Boolean(googleClientId));

2. Receive the ID Token on the Frontend, Save It, and Pass It to the API

Upon successful GIS login, receive the credential (ID token) and save it to localStorage.

// src/App.vue
function handleGoogleCredential(response) {
  const credential = response?.credential;
  if (!credential) {
    return;
  }
  localStorage.setItem(ID_TOKEN_STORAGE_KEY, credential);
  idToken.value = credential;
  portfolioStore.fetchPortfolio();
}

When calling the API, append the id_token as a query parameter.

// src/stores/portfolio.js
function buildApiUrlWithToken() {
  const idToken = getGoogleIdToken();
  if (!idToken) {
    return API_URL;
  }

  const url = new URL(API_URL);
  url.searchParams.set("id_token", idToken);
  return url.toString();
}

For gh-pages delivery, using query parameters is less likely to run into CORS/preflight issues than using the Authorization header.

3. Support Token Extraction from Both Headers and Queries in GAS

First, extract the token on the GAS side. Prioritize Authorization: Bearer ... if it exists; otherwise, use the id_token query parameter.

// GAS: Code.gs
function extractIdToken_(event) {
  const headers = event?.headers || {};
  const auth = headers.Authorization || headers.authorization || '';
  const bearer = auth.match(/^Bearer\s+(.+)$/i);
  if (bearer) return bearer[1];

  return event?.parameter?.id_token || '';
}

4. Strictly Verify the ID Token in GAS

The core of this configuration is verifyGoogleIdTokenOrThrow_. An error is thrown immediately if any of the following are not met:

  • id_token exists
  • tokeninfo response is 200
  • iss matches Google's official value
  • aud matches the OAuth client ID
  • exp is in the future relative to current time
  • email_verified is true
  • Only emails included in AVAILABLE_GMAILS are permitted
// GAS: Code.gs
function verifyGoogleIdTokenOrThrow_(idToken) {
  if (!idToken) {
    throw new Error('missing id token');
  }

  const oauthClientId = getScriptProperty_('GOOGLE_OAUTH_CLIENT_ID');
  if (!oauthClientId) {
    throw new Error('missing GOOGLE_OAUTH_CLIENT_ID');
  }

  const response = UrlFetchApp.fetch(
    `https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(idToken)}`,
    { muteHttpExceptions: true },
  );

  if (response.getResponseCode() !== 200) {
    throw new Error('token verification failed');
  }

  const payload = JSON.parse(response.getContentText());
  // Verify iss / aud / exp / email_verified / allowlist
}

5. Execute the Authentication Gate First in doGet

Run the authentication check near the beginning of doGet before proceeding to return data. This eliminates paths where data can be accessed without authentication.

// GAS: Code.gs
export function doGet(e) {
  try {
    const parameters = e?.parameter;

    if (!isDebugMode_()) {
      const idToken = extractIdToken_(e);
      verifyGoogleIdTokenOrThrow_(idToken);
    }

    // Proceed to cache/live data processing only if authentication succeeds
  } catch (error) {
    const message = error?.message || 'unauthorized';
    const status = message === 'forbidden email' ? 403 : 401;
    return createJsonResponse_(JSON.stringify({ status, error: message }));
  }
}

The frontend treats these 401/403 responses as AUTH errors and redirects the user back to the login flow.

// src/stores/portfolio.js
if (json?.status === 401 || json?.status === 403) {
  const authMessage = json.error ?? "unauthorized";
  throw new Error(`AUTH ${json.status}: ${authMessage}`);
}

Challenges Faced and Solutions

Challenge 1: aud Mismatches are Prone to Occur Due to OAuth Configuration Errors

It is easy to fail due to misidentifying the Client ID (e.g., using an ID from a different project).

Solution

  • Manage GOOGLE_OAUTH_CLIENT_ID in GAS and VITE_GOOGLE_CLIENT_ID on the frontend using the same value.
  • On failure, make the cause visible by explicitly identifying an invalid aud in GAS.

Challenge 2: Management of Authorized Users Becomes Dependent on Specific Individuals

Initially, these are often hardcoded, making operational changes cumbersome.

Solution

  • Manage AVAILABLE_GMAILS via Script Properties.
  • Allow the operations team to edit them in a format like a@example.com,b@example.com.

Challenge 3: Risk of DEBUG Mode leaking into Production

If DEBUG remains true, there is a risk of bypassing authentication.

Solution

  • Make setting DEBUG=false a mandatory item in the pre-deployment checklist for production.
  • Separate GAS deployments for production and verification.

Summary

Even with gh-pages + GAS, Google OAuth authentication can be built at a sufficiently practical level. The three key points are as follows:

  1. Fix the OAuth setup (Client ID) first
  2. Perform strict token verification in GAS
  3. Convert operational settings like allowed emails and DEBUG into Properties

In the future, I plan to further improve operational quality by adding authentication audit logs and alert integration (detection of consecutive failures).

Discussion