- Notifications
You must be signed in to change notification settings - Fork456
An OAuth2 library for Google Apps Script.
License
googleworkspace/apps-script-oauth2
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
OAuth2 for Apps Script is a library for Google Apps Script that provides theability to create and authorize OAuth2 tokens as well as refresh them when theyexpire. This library uses Apps Script'sStateTokenBuilderand/usercallback endpoint to handle the redirects.
If you are trying to connect to a Google API from Apps Script you might not needto use this library at all. Apps Script has a number of easy-to-use,built-in services, as well as a variety ofadvanced services that wrap existing Google REST APIs.
Even if your API is not covered by either, you can still use Apps Script toobtain the OAuth2 token for you. Simplyedit the script's manifest toinclude the additional scopes that your API requires.When the user authorizes your script they will also be asked to approve thoseadditional scopes. Then use the methodScriptApp.getOAuthToken()in your code to access the OAuth2 access token the script has acquired and passit in theAuthorization header of aUrlFetchApp.fetch() call.
Visit the sampleNoLibrary to see an example of how thiscan be done.
This library is already published as an Apps Script, making it easy to includein your project. To add it to your script, do the following in the Apps Scriptcode editor:
- Click on the menu item "Resources > Libraries..."
- In the "Find a Library" text box, enter the script ID
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDFand click the"Select" button. - Choose a version in the dropdown box (usually best to pick the latestversion).
- Click the "Save" button.
Alternatively, you can copy and paste the files in the/dist directorydirectly into your script project.
If you aresetting explicit scopesin your manifest file, ensure that the following scope is included:
https://www.googleapis.com/auth/script.external_request
Before you can start authenticating against an OAuth2 provider, you usually needto register your application with that OAuth2 provider and obtain a client IDand secret. Often a provider's registration screen requires you to enter a"Redirect URI", which is the URL that the user's browser will be redirected toafter they've authorized access to their account at that provider.
For this library (and the Apps Script functionality in general) the URL willalways be in the following format:
https://script.google.com/macros/d/{SCRIPT ID}/usercallbackWhere{SCRIPT ID} is the ID of the script that is using this library. Youcan find your script's ID in the Apps Script code editor by clicking onthe menu item "File > Project properties".
Alternatively you can call the service'sgetRedirectUri() method to view theexact URL that the service will use when performing the OAuth flow:
/** * Logs the redirect URI to register. */functionlogRedirectUri(){varservice=getService_();Logger.log(service.getRedirectUri());}
Using the library to generate an OAuth2 token has the following basic steps.
The OAuth2Service class contains the configuration information for a givenOAuth2 provider, including its endpoints, client IDs and secrets, etc. Thisinformation is not persisted to any data store, so you'll need to create thisobject each time you want to use it. The example below shows how to create aservice for the Google Drive API.
Ensure the method is private (has an underscore at the end of the name) toprevent clients from being able to call the method to read your client ID andsecret.
functiongetDriveService_(){// Create a new service with the given name. The name will be used when// persisting the authorized token, so ensure it is unique within the// scope of the property store.returnOAuth2.createService('drive')// Set the endpoint URLs, which are the same for all Google services..setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth').setTokenUrl('https://accounts.google.com/o/oauth2/token')// Set the client ID and secret, from the Google Developers Console..setClientId('...').setClientSecret('...')// Set the name of the callback function in the script referenced// above that should be invoked to complete the OAuth flow..setCallbackFunction('authCallback')// Set the property store where authorized tokens should be persisted..setPropertyStore(PropertiesService.getUserProperties())// Set the scopes to request (space-separated for Google services)..setScope('https://www.googleapis.com/auth/drive')// Below are Google-specific OAuth2 parameters.// Sets the login hint, which will prevent the account chooser screen// from being shown to users logged in with multiple accounts..setParam('login_hint',Session.getEffectiveUser().getEmail())// Requests offline access..setParam('access_type','offline')// Consent prompt is required to ensure a refresh token is always// returned when requesting offline access..setParam('prompt','consent');}
Apps Script UI's are not allowed to redirect the user's window to a new URL, soyou'll need to present the authorization URL as a link for the user to click.The URL is generated by the service, using the functiongetAuthorizationUrl().
functionshowSidebar(){vardriveService=getDriveService_();if(!driveService.hasAccess()){varauthorizationUrl=driveService.getAuthorizationUrl();vartemplate=HtmlService.createTemplate('<a href="<?= authorizationUrl ?>">Authorize</a>. '+'Reopen the sidebar when the authorization is complete.');template.authorizationUrl=authorizationUrl;varpage=template.evaluate();DocumentApp.getUi().showSidebar(page);}else{// ...}}
When the user completes the OAuth2 flow, the callback function you specifiedfor your service will be invoked. This callback function should pass itsrequest object to the service'shandleCallback function, and show a messageto the user.
functionauthCallback(request){vardriveService=getDriveService_();varisAuthorized=driveService.handleCallback(request);if(isAuthorized){returnHtmlService.createHtmlOutput('Success! You can close this tab.');}else{returnHtmlService.createHtmlOutput('Denied. You can close this tab');}}
If the authorization URL was opened by the Apps Script UI (via a link, button,etc) it's possible to automatically close the window/tab usingwindow.top.close(). You can see an example of this in the sample add-on'sCallback.html.
Now that the service is authorized you can use its access token to makerequests to the API. The access token can be passed along with aUrlFetchApprequest in the "Authorization" header.
functionmakeRequest(){vardriveService=getDriveService_();varresponse=UrlFetchApp.fetch('https://www.googleapis.com/drive/v2/files?maxResults=10',{headers:{Authorization:'Bearer '+driveService.getAccessToken()}});// ...}
To logout the user or disconnect the service, perhaps so the user can select adifferent account, use thereset() method:
functionlogout(){varservice=getDriveService_()service.reset();}
In almost all cases you'll want to persist the OAuth tokens after you retrievethem. This prevents having to request access from the user every time you wantto call the API. To do so, make sure you set a properties store when you defineyour service:
returnOAuth2.createService('Foo').setPropertyStore(PropertiesService.getUserProperties())// ...
Apps Script hasproperty stores scoped to the user, script,or document. In most cases you'll want to choose user-scoped properties, as itis most common to have each user of your script authorize access to their ownaccount. However there are uses cases where you'd want to authorize access toa shared resource and then have all users of the script (or on the samedocument) share that access.
When using a service account or 2-legged OAuth flow, where users aren't promptedfor authorization, storing tokens is still beneficial as there can be ratelimits on generating new tokens. However there are edge cases where you need togenerate lots of different tokens in a short amount of time, and persistingthose tokens to properties can exceed yourPropertiesService quota. In thosecases you can omit any form of token storage and just retrieve new ones asneeded.
Scripts that use the library heavily should enable caching on the service, so asto not exhaust theirPropertiesService quotas. To enable caching, simply addaCacheService cache when configuring the service:
returnOAuth2.createService('Foo').setPropertyStore(PropertiesService.getUserProperties()).setCache(CacheService.getUserCache())// ...
Make sure to select a cache with the same scope (user, script, or document) asthe property store you configured.
A race condition can occur when two or more script executions are both trying torefresh an expired token at the same time. This is sometimes observed inGmail Add-ons, where a userquickly paging through their email can trigger the same add-on multiple times.
To prevent this, use locking to ensure that only one execution is refreshingthe token at a time. To enable locking, simply add aLockService lock whenconfiguring the service:
returnOAuth2.createService('Foo').setPropertyStore(PropertiesService.getUserProperties()).setCache(CacheService.getUserCache()).setLock(LockService.getUserLock())// ...
Make sure to select a lock with the same scope (user, script, or document) asthe property store and cache you configured.
See below for some features of the library you may need to utilize depending onthe specifics of the OAuth provider you are connecting to. See thegeneratedreference documentationfor a complete list of methods available.
OAuth services can return a token in two ways: as JSON or an URL encodedstring. You can set which format the token is in withsetTokenFormat(tokenFormat). There are two ENUMS to set the mode:TOKEN_FORMAT.FORM_URL_ENCODED andTOKEN_FORMAT.JSON. JSON is set as defaultif no token format is chosen.
Some services, such as the FitBit API, require you to set an Authorizationheader on access token requests. ThesetTokenHeaders() method allows youto pass in a JavaScript object of additional header key/value pairs to be usedin these requests.
.setTokenHeaders({'Authorization':'Basic '+Utilities.base64Encode(CLIENT_ID+':'+CLIENT_SECRET)});
See theFitBit sample for the complete code.
Almost all services use thePOST HTTP method when retrieving the access token,but a few services deviate from the spec and use thePUT method instead. Toaccomodate those cases you can use thesetTokenMethod() method to specify theHTTP method to use when making the request.
Some OAuth providers, such as the Smartsheet API, require you toadd a hash to the access token request payloads.ThesetTokenPayloadHandler method allows you to pass in a function to modifythe payload of an access token request before the request is sent to the tokenendpoint:
// Set the handler for modifying the access token request payload:.setTokenPayloadHandler(myTokenHandler)
See theSmartsheet sample for the complete code.
Some OAuth providers return IDs and other critical information in the callbackURL along with the authorization code. While it's possible to capture and storethese separately, they often have a lifecycle closely tied to that of the tokenand it makes sense to store them together. You can useService.getStorage() toretrieve the token storage system for the service and set custom key-valuepairs.
For example, the Harvest API returns the account ID of the authorized accountin the callback URL. In the following code the account ID is extracted from therequest parameters and saved saved into storage.
functionauthCallback(request){varservice=getService_();varauthorized=service.handleCallback(request);if(authorized){// Gets the authorized account ID from the scope string. Assumes the// application is configured to work with single accounts. Has the format// "harvest:{ACCOUNT_ID}".varscope=request.parameter['scope'];varaccountId=scope.split(':')[1];// Save the account ID in the service's storage.service.getStorage().setValue('Harvest-Account-Id',accountId);returnHtmlService.createHtmlOutput('Success!');}else{returnHtmlService.createHtmlOutput('Denied.');}}
When making an authorized request the account ID is retrieved from storage andpassed via a header.
if(service.hasAccess()){// Retrieve the account ID from storage.varaccountId=service.getStorage().getValue('Harvest-Account-Id');varurl='https://api.harvestapp.com/v2/users/me';varresponse=UrlFetchApp.fetch(url,{headers:{'Authorization':'Bearer '+service.getAccessToken(),'User-Agent':'Apps Script Sample','Harvest-Account-Id':accountId}});
Note that callingService.reset() will remove all custom values from storage,in addition to the token.
There are occasionally cases where you need to preserve some data through theOAuth flow, so that it is available in your callback function. Although youcould use the token storage mechanism discussed above for that purpose, writingto the PropertiesService is expensive and not neccessary in the case where theuser doesn't start or fails to complete the OAuth flow.
As an alternative you can store small amounts of data in the OAuth2statetoken, which is a standard mechanism for this purpose. To do so, pass anoptional hash of parameter names and values to thegetAuthorizationUrl()method:
varauthorizationUrl=getService_().getAuthorizationUrl({// Pass the additional parameter "lang" with the value "fr".lang:'fr'});
These values will be stored along-side Apps Script's internal information in theencyptedstate token, which is passed in the authorization URL and passed backto the redirect URI. Thestate token is automatically decrypted in thecallback function and you can access your parameters using the samerequest.parameter field used in web apps:
functionauthCallback(request){varlang=request.parameter.lang;// ...}
This library supports the service account authorization flow, also known as theJSON Web Token (JWT) Profile.This is a two-legged OAuth flow that doesn't require a user to visit a URL andauthorize access.
One common use for service accounts with Google APIs isdomain-wide delegation.This process allows a G Suite domain administrator to grant anapplication access to all the users within the domain. When the applicationwishes to access the resources of a particular user, it uses the service accountauthorization flow to obtain an access token. See the sampleGoogleServiceAccount.gs for moreinformation.
Although optimized for the authorization code (3-legged) and service account(JWT bearer) flows, this library supports arbitrary flows using thesetGrantType() method. UsesetParam() orsetTokenPayloadHandler() to addfields to the token request payload, andsetTokenHeaders() to add any requiredheaders.
The most common of these is theclient_credentials grant type, which oftenrequires that the client ID and secret are passed in the Authorization header.When using this grant type, if you set a client ID and secret usingsetClientId() andsetClientSecret() respectively then anAuthorization: Basic ... header will be added to the token requestautomatically, since this is what most OAuth2 providers require. If yourprovider uses a different method of authorization then don't set the client IDand secret and add an authorization header manually.
See the sampleTwitterAppOnly.gs for a workingexample.
The service name passed in to thecreateService method forms part of the keyused when storing and retrieving tokens in the property store. To connect tomultiple services merely ensure they have different service names. Often thismeans selecting a service name that matches the API the user will authorize:
functionrun(){vargitHubService=getGitHubService_();varmediumService=getMediumService_();// ...}functiongetGitHubService_(){returnOAuth2.createService('GitHub')// GitHub settings ...}functiongetMediumService_(){returnOAuth2.createService('Medium')// Medium settings ...}
Occasionally you may need to make multiple connections to the same API, forexample if your script is trying to copy data from one account to another. Inthose cases you'll need to devise your own method for creating unique servicenames:
functionrun(){varcopyFromService=getGitHubService_('from');varcopyToService=getGitHubService_('to');// ...}functiongetGitHubService_(label){returnOAuth2.createService('GitHub_'+label)// GitHub settings ...}
You can list all of the service names you've previously stored tokens for usingOAuth2.getServiceNames(propertyStore).
This library was designed to work with any OAuth2 provider, but because of smalldifferences in how they implement the standard it may be that some APIsaren't compatible. If you find an API that it doesn't work with, open an issueor fix the problem yourself and make a pull request against the source code.
This library is designed for server-side OAuth flows, and client-side flows withimplicit grants (response_type=token) are not supported.
- Version 20 - Switched from using project keys to script IDs throughout thelibrary. When upgrading from an older version, ensure the callback URLregistered with the OAuth provider is updated to use the format
https://script.google.com/macros/d/{SCRIPT ID}/usercallback. - Version 22 - Renamed
Service.getToken_()toService.getToken(), sincethere OAuth providers that return important information in the token response.
You aresetting explicit scopesin your manifest file but have forgotten to add thehttps://www.googleapis.com/auth/script.external_request scope used by this library(and eventually theUrlFetchApp request you are making to an API).
About
An OAuth2 library for Google Apps Script.
Topics
Resources
License
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.