iTranslated by AI
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_tokenverification 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
- Create a project in Google Cloud
- Set up the OAuth consent screen (enter required information)
- Create an OAuth 2.0 Client ID (Web Application)
- 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
Authorizationheader.
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_tokenexists -
tokeninforesponse is 200 -
issmatches Google's official value -
audmatches the OAuth client ID -
expis in the future relative to current time -
email_verifiedis true - Only emails included in
AVAILABLE_GMAILSare 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_IDin GAS andVITE_GOOGLE_CLIENT_IDon the frontend using the same value. - On failure, make the cause visible by explicitly identifying an
invalid audin GAS.
Challenge 2: Management of Authorized Users Becomes Dependent on Specific Individuals
Initially, these are often hardcoded, making operational changes cumbersome.
Solution
- Manage
AVAILABLE_GMAILSvia 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=falsea 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:
- Fix the OAuth setup (Client ID) first
- Perform strict token verification in GAS
- 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