Handle OAuth 2.0 in Apps Script
Connect Northwind to APIs that require user authorisation — using the OAuth2 library.
Published Jun 29, 2025
Plenty of the APIs Northwind wants to reach — accounting tools, project trackers, CRMs — do not accept a static API key. They require OAuth 2.0: the user is sent to the provider’s site, approves access, and the provider hands back a token your script then uses on their behalf. Doing that by hand means juggling redirect URLs, authorisation codes, token exchanges and refreshes, and it is easy to get subtly wrong.
This article wires up the community OAuth2 library, which handles the whole dance. You define the service once, the user clicks an authorisation link the first time, and from then on every call uses a token the library refreshes automatically. The example connects to a generic “my-api” provider; swap the endpoints for the real ones and it works the same.
What you’ll need
- The OAuth2 library installed in your project (steps below).
- A registered application with the provider, giving you a client ID and a client secret. Store both in Script Properties rather than in source — see Store API keys and secrets securely.
- The provider’s authorisation URL, token URL and the scopes your script needs.
- Your script’s redirect URI added to the provider’s allowed list — it must be
exactly
https://script.google.com/macros/d/SCRIPT_ID/usercallback.
Install the library
- In the Apps Script editor, click Libraries (the
+next to Libraries in the left sidebar). - Paste the library script ID:
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF. - Click Look up, pick the latest version, and add it with the identifier
OAuth2.
The script
// Provider endpoints — replace with the real values from the API's docs.
const AUTH_BASE_URL = 'https://provider.example.com/oauth/authorize';
const TOKEN_URL = 'https://provider.example.com/oauth/token';
// Space-separated scopes the script needs.
const OAUTH_SCOPE = 'read write';
// A short name for this service. The OAuth2 library uses it to namespace
// the stored token, so keep it stable across the project.
const SERVICE_NAME = 'my-api';
/**
* Builds the OAuth2 service. Every function that touches the provider
* calls this — defining it in one place keeps the config consistent.
*
* @returns {OAuth2.Service} The configured, reusable service object.
*/
function getMyService() {
// Client ID and secret live in Script Properties, never in source.
const props = PropertiesService.getScriptProperties();
const clientId = props.getProperty('OAUTH_CLIENT_ID');
const clientSecret = props.getProperty('OAUTH_CLIENT_SECRET');
return OAuth2.createService(SERVICE_NAME)
.setAuthorizationBaseUrl(AUTH_BASE_URL)
.setTokenUrl(TOKEN_URL)
.setClientId(clientId)
.setClientSecret(clientSecret)
// The library calls this function when the provider redirects back.
.setCallbackFunction('authCallback')
// Tokens are kept per user, so each Northwind user authorises once.
.setPropertyStore(PropertiesService.getUserProperties())
.setScope(OAUTH_SCOPE);
}
/**
* Logs the authorisation URL the user must visit to grant access.
* Run this once to start the connection.
*/
function showAuthorizationUrl() {
const service = getMyService();
if (service.hasAccess()) {
Logger.log('Already connected — no action needed.');
return;
}
Logger.log('Open this URL to authorise:\n' + service.getAuthorizationUrl());
}
/**
* The OAuth2 library redirects the user here after they approve (or deny)
* access. It completes the token exchange and shows a result page.
*
* @param {Object} request The callback request from the provider.
* @returns {HtmlOutput} A page confirming success or failure.
*/
function authCallback(request) {
const isAuthorised = getMyService().handleCallback(request);
return HtmlService.createHtmlOutput(
isAuthorised
? 'Connected. You can close this tab.'
: 'Authorisation failed. Please try again.'
);
}
/**
* Makes an authenticated request to the provider. The OAuth2 library
* supplies a valid access token and refreshes it transparently.
*
* @param {string} path The API path to call, e.g. '/v1/projects'.
* @returns {Object} The parsed JSON response.
*/
function callApi(path) {
const service = getMyService();
// No token yet — point the caller at the authorisation step.
if (!service.hasAccess()) {
throw new Error('Not connected. Run showAuthorizationUrl first.');
}
const response = UrlFetchApp.fetch('https://provider.example.com' + path, {
headers: { Authorization: 'Bearer ' + service.getAccessToken() },
muteHttpExceptions: true,
});
return JSON.parse(response.getContentText());
}
/**
* Disconnects the current user, clearing their stored token. Useful for
* testing, or when a user wants to revoke access.
*/
function resetAuth() {
getMyService().reset();
Logger.log('Authorisation cleared for the current user.');
}
How it works
getMyServiceis the single place the OAuth2 service is defined. It pulls the client ID and secret from Script Properties and wires in the provider’s authorisation and token URLs, the scopes, and the callback function name.- The property store is set to
getUserProperties, so each Northwind user’s token is stored against their own account. One user authorising does not connect everyone else. showAuthorizationUrlchecks whether the current user already has access. If not, it logs the URL they must visit to grant it.- When the user approves at the provider, the provider redirects to the
script’s
usercallbackURL. The library routes that toauthCallback, which callshandleCallbackto exchange the authorisation code for an access token and stores it. callApimakes authenticated requests. It callsgetAccessToken, which returns a valid token — refreshing it behind the scenes if it has expired — and attaches it as aBearerheader.resetAuthclears the stored token for the current user, which is handy for testing the flow end to end or letting a user disconnect.
Example run
First-time setup, run once per user:
- Run
showAuthorizationUrl. It logs an authorisation URL. - Open the URL, sign in at the provider and approve the requested scopes.
- The provider redirects back;
authCallbackshows “Connected.”
After that, calls just work:
const projects = callApi('/v1/projects');
Logger.log('Fetched ' + projects.length + ' projects.');
| Stage | What happens |
|---|---|
| Before authorising | callApi throws “Not connected.” |
| After authorising | callApi returns data; token refreshed silently as needed |
After resetAuth | Back to “Not connected.” until the user re-authorises |
Run it
OAuth flows are interactive, so they run on demand, not on a trigger:
- Save the client ID and secret in Project Settings → Script Properties as
OAUTH_CLIENT_IDandOAUTH_CLIENT_SECRET. - Run
showAuthorizationUrl, open the logged URL, and approve access. - Run
callApi(or any function that calls it) to use the connection.
To make this usable by non-editors, expose showAuthorizationUrl through a
custom menu or a web app so each user can authorise themselves without opening
the script editor.
Watch out for
- The redirect URI must be registered with the provider exactly as
https://script.google.com/macros/d/SCRIPT_ID/usercallback, using the script’s own ID. A mismatched URI is the most common cause of a failed callback. - Tokens are stored per user via
getUserProperties. A token from one user is never available to another — by design — so every user authorises once. - Refresh handling is automatic, but only if the provider issued a refresh token. Some providers only return one when you request offline access or a specific scope — check the provider’s docs if connections silently expire.
- If you change the client ID, secret, scopes or
SERVICE_NAME, existing stored tokens become invalid. RunresetAuthand have users re-authorise. - Never log or expose
getAccessToken’s return value. A leaked token grants the same access the user approved. - The OAuth2 library is community-maintained, not part of Apps Script. Pin a specific version rather than tracking the head, so an upstream change cannot break the connection without warning.
Related
Handle streaming responses from an LLM API
Manage long Northwind AI outputs reliably — note: Apps Script UrlFetch is synchronous.
Updated Jan 3, 2026
Cache API responses to cut quota usage
Store and reuse Northwind API responses intelligently — sub-second hits, fewer bills.
Updated Dec 26, 2025
Build an API-key vault and rotation system
Manage Northwind credentials securely at scale — centralised storage, scheduled rotation.
Updated Dec 22, 2025
Build a rate-limit-aware API client
Back off and retry gracefully on 429s — Northwind's robust outbound HTTP pattern.
Updated Dec 14, 2025
Build a generic paginated-API fetcher
Handle cursors and pages for any large dataset — Northwind's standard pull pattern.
Updated Dec 6, 2025