Deployment access settings are coarse. Adding an allowlist of email domains in doGet gives a second gate for internal tools.
isAllowedDomain_ compares the substring after @ against ALLOWED_DOMAINS case-insensitively.
saveNote calls requireDomain_ before appendRow so google.script.run cannot bypass the HTML gate.
Combine with Execute as: User accessing and domain-only deployment when everyone is on Google Workspace.
Need this built? Hire a Google Apps Script developer →
Sheet / project setup
| Item | Value | Purpose |
|---|---|---|
| ALLOWED_DOMAINS | example.com, example.org | Permit list |
| App.html | UI | Only rendered for allowed users |
| Notes | A–C | Timestamp, email, note text |
| Deploy access | Anyone in domain / Google | First gate |
What this script does
doGet shows Access denied for wrong domains; allowed users receive App.html.
requireDomain_ centralizes the check for every write endpoint.
Prerequisites
Web app deployment and an App.html file; Users signed into Google.
- Correct ALLOWED_DOMAINS
- Execute as user accessing
- Notes sheet for the sample write
Walkthrough
Open the URL with a company account and a personal Gmail; confirm deny vs App.html. Submit a note and verify Notes rows.
Edge cases
Subdomains like mail.example.com are different domains — list them explicitly or parse carefully.
- Empty email when signed out
- Aliases: still same domain string after @
How to test
Use two test accounts on different domains; attempt saveNote from the browser console on a denied session.
Hardening for production
Also check group membership via Admin Directory for finer roles.
Variations
Allowlist exact emails instead of domains for vendor contractors.
Full code: doGet() + saveNote()
Set ALLOWED_DOMAINS, add App.html, deploy as user-accessing, then verify deny/allow with two accounts.
/**
* Reject web app access unless the active user's email domain matches.
*/
const ALLOWED_DOMAINS = ["example.com", "example.org"];
function doGet(e) {
const email = Session.getActiveUser().getEmail();
if (!email || !isAllowedDomain_(email)) {
return HtmlService.createHtmlOutput(
"<h1>Access denied</h1><p>Use your company Google account.</p>");
}
const html = HtmlService.createHtmlOutputFromFile("App");
html.setTitle("Internal tools");
return html;
}
function isAllowedDomain_(email) {
const domain = String(email).split("@")[1] || "";
return ALLOWED_DOMAINS.some(function (d) {
return d.toLowerCase() === domain.toLowerCase();
});
}
function requireDomain_() {
const email = Session.getActiveUser().getEmail();
if (!isAllowedDomain_(email)) {
throw new Error("Forbidden domain: " + email);
}
return email;
}
/** Example mutating endpoint */
function saveNote(text) {
requireDomain_();
const sheet = SpreadsheetApp.getActive().getSheetByName("Notes");
sheet.appendRow([new Date(), Session.getActiveUser().getEmail(), text]);
return { ok: true };
}- Line 4: Company domains permitted to load the app.
- Line 8: Compares email domain case-insensitively.
- Line 10: HTML returned to blocked visitors.
- Line 24: Shared guard for mutating server calls.
- Line 33: Example write path that re-checks domain.
- Line 36: Stores note only after the domain guard passes.
Deploy this example
- 01
Open Apps Script
In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.
- 02
Paste and save
Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 04
Add the trigger
Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.
Before you run: domain gate
- 1ALLOWED_DOMAINS lists every corporate domain you use
- 2App.html present
- 3Deployment uses User accessing
- 4Denied UX tested with an outside account
- 5All google.script.run write methods call requireDomain_
- 6Notes sheet exists for the sample
Frequently asked questions
It helps, but server-side checks stop mistaken Anyone access and protect API-like endpoints.
Add both domains to ALLOWED_DOMAINS as in the sample.
Treat them as distinct strings unless you normalize consumer aliases deliberately.
Not for Session.getActiveUser — that is server-side. Still never trust a client-supplied email parameter.
getActiveUser may be blank for visitors. Domain gating needs execute-as-user.
Append to an AccessLog sheet inside the deny branch with timestamp and email (if any).
Yes — after domain checks, call Admin Directory Groups.members to require group membership.