Unlike many other environments in Google Cloud that provide default application credentials, Apps Script is built on OAuth and user credentials. However there are many cases, where a service account is needed to access Google Cloud resources. For example, a service account is needed to interact with the Google Chat API as a Chat App.
Instead of downloading the service account key and storing it in the Apps Script project, the service account can be impersonated using the ScriptApp.getOAuthToken()
and user as principal. This allows the service account to be used without downloading the key.
Setup service account impersonation and Apps Script
There a few steps to get this working right in Apps Script:
- Create a service account in the Google Cloud project
- Grant the principal (your account or whoever executes the script) access to the service account
- Add the
Service Account Token Creator
role to the principal (Owner
role is not sufficient) - Enable the IAM Service Account Credentials API in the Google Cloud project
- Add the Google Cloud project number to the Apps Script project settings
- Add the following scopes to the Apps Script project manifest:
{
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/cloud-platform"
]
}
A more detailed explanation of these steps can be found in the Create short-lived credentials for a service account.
IAM Service Account Credentials API and impersonation
To generate the OAuth token for the service account, the generateAccessToken
endpoint of the IAM Credentials API is used. Calling this endpoint requires code similar to the following using UrlFetchApp and ScriptApp.getOAuthToken()
:
/**
* Generates an access token using impersonation. Requires the following:
*
* - Service Account Token Creator
* - IAM Credentials API
*
* @params {string} serviceAccountEmail
* @params {Array<string>} scope
* @params {string} [lifetime="3600s"]
* @returns {string}
*/
function generateAccessTokenForServiceAccount(
serviceAccountEmailOrId,
scope,
lifetime = "3600s", // default
) {
const host = "https://iamcredentials.googleapis.com";
const url = `${host}/v1/projects/-/serviceAccounts/${serviceAccountEmailOrId}:generateAccessToken`;
const payload = {
scope,
lifetime,
};
const options = {
method: "POST",
headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() },
contentType: "application/json",
muteHttpExceptions: true,
payload: JSON.stringify(payload),
};
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() < 300) {
return JSON.parse(response.getContentText()).accessToken;
} else {
throw new Error(response.getContentText());
}
}
This function can be used to generate an access token for the service account. The access token can then be used to make requests to Google Cloud APIs.
Generating and using service account access tokens in Apps Script
Now I can use this function to generate an access token for the service account and verify it contains valid scopes:
function main() {
const token = generateAccessTokenForServiceAccount(
// can also be the email: foo@your-project.iam.gserviceaccount.com
"112304111718889638064",
["https://www.googleapis.com/auth/datastore"]
);
// verify the token
console.log(
UrlFetchApp.fetch(
`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${token}`,
).getContentText(),
);
}
The output looks like the following:
12:53:12 PM Notice Execution started
12:53:13 PM Info ya29.c.c0AY_VpZ... // truncated
12:53:13 PM Info {
"issued_to": "112304111718889638064",
"audience": "112304111718889638064",
"scope": "https://www.googleapis.com/auth/datastore",
"expires_in": 3599,
"access_type": "online"
}
12:53:14 PM Notice Execution completed
To use this token to make requests to Google Cloud APIs, the token can be added to the Authorization
header of the request instead of the ScriptApp.getOAuthToken()
user token:
const options = {
headers: { Authorization: `Bearer ${token}` },
};
UrlFetchApp.fetch(url, options);
Be sure to update the scopes in the generateAccessTokenForServiceAccount
call to match the scopes needed for the request.
Top comments (1)
Thanks for your detailed guide. I would like to achieve the above but not linking my app script to one GCP. How would this be possible? Thanks for your answer in advance.