The public surface you call from application code. Server functions live in auth-adapter.js (the interface), auth-service.js (the role hierarchy helper), auth-scopes.js (optional) and auth-setup.js (admin). The client helpers live in client/auth-api.html.
Call the adapter, not the internal functions. Anything ending in an underscore (getUserFromSheet_) is internal and may change. The trailing underscore is Apps Script’s own privacy rule: those functions cannot be called through google.script.run, while every other top-level function can. See Security for what that means for your own code.
Server: guards and queries
requireAuth(requiredRole?)
Guards a server route. Returns the auth object on success, throws on failure. This is the only layer that carries real security weight. Put it at the top of every privileged server function.
requiredRolestring– optional, defaults toAUTH_CONFIG.DEFAULT_REQUIRED_ROLE.- Returns
{ valid: true, role, email }. - Throws
Errorwith anAUTH_ERROR:prefixed message if the user is unidentified or under-privileged.
function deleteRecord(id) {
const auth = requireAuth('admin'); // throws unless caller is admin or above
// ... privileged work, auth.email is the verified actor
}
getCurrentUserAuth()
Resolves the current user’s auth for the active auth mode. Does not throw. Use when you want to branch on the result rather than block.
- Returns
{ valid, role?, email?, reason? }. The shape is identical in both auth modes. It is deliberately minimal because this function is reachable from the browser.
const auth = getCurrentUserAuth();
if (!auth.valid) {
return { ok: false, why: auth.reason };
}
hasRole(userAuth, requiredRole)
Non-throwing role check against an already-resolved auth object.
userAuthObject– fromgetCurrentUserAuth().requiredRolestring.- Returns
boolean.
const auth = getCurrentUserAuth();
const canEdit = hasRole(auth, 'editor');
getUserInfo()
Client-facing user info. Requires at least the default role, so calling it from the browser doubles as a session check.
- Returns
{ email, role, roles, valid: true }.rolesis the hierarchy ordered lowest privilege first, so the client never keeps its own copy. - Throws
AUTH_ERROR:if the caller is not authorised.
roleSatisfies(userRole, requiredRole)
The hierarchy primitive. True if userRole sits at or above requiredRole by the numeric ranks in AUTH_CONFIG.ROLE_RANKS. An unrecognised role on either side returns false, so it fails closed.
- Returns
boolean.
DEBUG_auth()
Editor diagnostic. Reports the active auth mode, the session email and what the resolution pipeline returns for the caller. Run it from the editor’s function dropdown – it is the owner-guarded entry point for the private debugAuth_(), which the dropdown cannot see. Refuses a non-owner caller under executeAs: USER_DEPLOYING, so google.script.run visitors cannot invoke it; under USER_ACCESSING, set OWNER_EMAIL to keep that guarantee (see Configuration).
- Logs the report to the execution log (the editor discards return values, so the log is the output).
- Returns
{ mode, sessionEmail, resolved }. - Throws when the caller is not the script owner.
A healthy sheet-mode reading, for a user listed in the auth sheet:
{
"mode": "sheet",
"sessionEmail": "you@yourdomain.com",
"resolved": {
"valid": true,
"role": "admin",
"email": "you@yourdomain.com"
}
}
What each field tells you:
mode– which auth mode the config selected:sheetordomain. If this is not what you expect, checkUSE_DOMAIN_AUTHin your config.sessionEmail– the identity Google reports for the session. WhenDEBUG_auth()succeeds this is always your own email: the owner guard refuses any session it cannot identify before the report is built. It cannot show you what a visitor’s identity would resolve to – whether a given visitor is identifiable is decided by the deployment settings and the visitor’s account; see the deployment matrix in Security.resolved– the fail-closed verdict. On failure it is{ valid: false, reason }instead, and thereasonstring says which check refused: for exampleUser not authorised(not in the sheet),User account is inactive(Active unticked) orOutside the permitted domain(domain mode). Avalid: falsefor a user you have not added yet is correct fail-closed behaviour, not a fault.
Server: scopes (optional)
From auth-scopes.js. A scope is an opaque tag, a tenant or team or region, attached to users and resources. See Architecture and Extending.
requireScope(scope, requiredRole?)
Guards a scoped route. Returns the auth object, or throws AUTH_ERROR: if the caller lacks the scope. Run after, or instead of, requireAuth().
function getTenantReport(tenantId) {
const auth = requireScope(tenantId, 'editor');
// caller is at least editor AND holds the tenantId scope
}
getUserScopes(email?)
Returns the user’s scopes as an array of tags. With no email it resolves the current user. Passing an explicit email reads another user’s scopes, so it requires admin and throws AUTH_ERROR: otherwise.
userHasScope(scope, email?)
Boolean check for one scope. The same rule as getUserScopes: no email means the current user, an explicit email requires admin.
setUserScopes(email, scopes)
Admin only. Sets a user’s scopes from an array or comma-separated string.
Server: setup and admin
From auth-setup.js.
| Function | Guard | Purpose |
|---|---|---|
SETUP_createAuthSheet() | owner only | Run once from the editor’s function dropdown. Creates the private auth sheet and logs its ID. Refuses non-owner callers under USER_DEPLOYING; under USER_ACCESSING, set OWNER_EMAIL to keep that guarantee. |
createAuthSheetTemplate_() | private | Does the actual sheet creation. The trailing underscore keeps it off google.script.run – it must run before any admin exists, so it cannot be admin-guarded – but it also hides it from the editor’s dropdown, hence the SETUP_ wrapper above. |
addUser(email, role?, active?, notes?) | admin | Adds a user. Role must be in AUTH_CONFIG.ROLES. |
setUserActive(email, active) | admin | Enables or disables a user without deleting the row. |
listUsers() | admin | Returns all users. lastLogin is an ISO string, or '' before first login. |
Client helpers
From client/auth-api.html. None of these is a security control. They improve the experience. The server guards are what keep people out.
invoke(functionName, params?)
Promise wrapper over google.script.run. Surfaces AUTH_ERROR: messages to the user and rejects with the original error.
invoke('saveSetting', ['hello'])
.then(result => console.log(result))
.catch(err => console.error(err));
api
The API object. Ships with getUserInfo. Extend it with your own endpoints:
Object.assign(api, {
saveSetting: (value) => invoke('saveSetting', [value]),
});
initUserSession()
Loads the current user once and caches it on window.currentUser. Call on page load. Returns a Promise of the user.
getCurrentUser() / hasUserRole(requiredRole)
getCurrentUser() returns the cached user or null. hasUserRole() mirrors the server hierarchy for UX checks, using the role order the server returns on the user object, so there is no separate list to keep in step.
requireRole(requiredRole, onAuthorized, onUnauthorized?)
Runs onAuthorized only if the loaded user meets requiredRole. Stops an unauthorised user landing on a half-built page. It protects nothing on its own. When initUserSession() has already loaded the user it gates on the cached copy, so several calls on one page cost one server round trip, not one each.