Three of the core parts within this kit are:
- An adapter that hides which auth mode is active.
- A role hierarchy expressed as one rank map.
- Three guard layers, of which only one carries security weight.
The adapter
Application code calls the adapter in auth-adapter.js and nothing else. The adapter routes to one of two auth modes based on AUTH_CONFIG.USE_DOMAIN_AUTH:
- Sheet-backed (
auth-service.js). Roles come from a private Google Sheet you own. Good when membership changes often and non-engineers manage it. - Domain / allowlist (in
auth-adapter.js). Roles come from the email and the allowlists in config. Useful when membership maps cleanly to a domain and a short admin list.
Both auth modes return the same core shape: { valid, role, email }. Both modes also run through the same hierarchy check, so your requireAuth('editor') calls behave identically whichever mode is live. You switch auth modes by flipping one flag with no change to application code.
Identity is resolved fresh on every call. There is no cache to clear, so a role change in the sheet or allowlist takes effect on the user’s next request. The reason the kit holds no cache is a security one and is covered in Security.
requireAuth('editor')
│
auth-adapter.js ── USE_DOMAIN_AUTH ? ──┐
│ │
sheet-backed domain / allowlist
(auth-service.js) (auth-adapter.js)
│ │
└──────────── roleSatisfies() ─────┘ ← one hierarchy, both paths
The role hierarchy
AUTH_CONFIG.ROLE_RANKS maps each role name to a numeric privilege rank. roleSatisfies compares ranks: a user satisfies a required role if their rank is greater than or equal to the required role’s rank. An unrecognised role on either side returns false, so the check fails closed.
This is the whole hierarchy. The ranks are explicit numbers rather than list order, so reformatting or reordering the map cannot silently change who outranks whom. The kit derives an ordered AUTH_CONFIG.ROLES list from the map for the sheet dropdown and validity checks, so there is still one source of truth. Adding a role is a one-line change to the map.
The three layers
The kit guards access at three layers. Only the first carries any security weight; the other two are conveniences.
| Layer | Where | What it is |
|---|---|---|
| 1. Server route guard | requireAuth() in each server function | Security. Runs on the server, cannot be bypassed, throws before any privileged work. |
| 2. Client navigation guard | requireRole() in the browser | UX. Stops an unauthorised user landing on a half-built page. Trivially bypassable. |
| 3. Conditional render | hidden panels, role-gated controls | Tidiness. Keeps privileged controls out of an unauthorised DOM. Not a control. |
Security states the same rule from the threat-model side.
How the code reaches your project
The kit is vendored: copy the files in src/ into your project and own them. Apps Script files share one global scope, so once the files sit alongside your code there is nothing to import, install or depend on at runtime. Configuration covers the one file you then create, config.js. For a working example of the kit wired into a web app, see examples/demo-webapp in the repository.
If you would rather not vendor, the server core can be published as a versioned Apps Script Library and called as AuthKit.requireAuth('editor'). The library route suits an organisation that wants one central copy of the server logic across many internal apps, with the auth policy baked into that copy – a library cannot read AUTH_CONFIG from the consuming script, because the two do not share a global scope. It costs added call latency, a thin wrapper in each consuming project for every function the browser calls (google.script.run cannot reach library functions), and the client helper must be vendored regardless – libraries ship server code only. For a boilerplate meant to be read and owned, vendoring is the simpler default.