Obtain an id token in the Gmail add-on for a backend service authentication - google-apps-script

The background
I'm using the Google Apps Script to create a Gmail Add-on.
Via this plugin, I would like to connect to my backend server (a non-Google service) using a REST service request. The request has to be authorised. When authorised, I could then make requests to that server to receive data associated with that user in the database. I'm already using Google sign-in in my webapp to sign in to the backend service - at the front end, I receive the id_token inside of the GoogleUser object in the authorisation response.
The problem
I need this id_token to log in to my backend service when connecting to it via the Gmail plugin. However, I couldn't find a way how to access the token.
The research
I would assume the token must be available through the API in the Apps Script.
In the webapp, I receive the id_token using the Google Auth API like this:
Promise.resolve(this.auth2.signIn())
.then((googleUser) => {
let user_token = googleUser.getAuthResponse().id_token; // this is the id_token I need in the Gmail plugin, too
// send the id_token to the backend service
...
};
In the Google Apps Script API I could only find the OAuth token:
ScriptApp.getOAuthToken();
I assumed the token could also be stored in the session. The Google Apps Script API contains the Session class and that itself contains the getActiveUser method, which returns the User object. The User object, however, only contains the user's email address, no id token (or anything else for that matter):
Session.getActiveUser().getEmail();
The question(s)
Is there a way to obtain the id token?
Am I choosing the right approach to logging in to the backend server using the data of the signed-in user in the Gmail?

Method 1: use getIdentityToken()
Gets an OpenID Connect identity token for the effective user:
var idToken = ScriptApp.getIdentityToken();
var body = idToken.split('.')[1];
var decoded = Utilities.newBlob(Utilities.base64Decode(body)).getDataAsString();
var payload = JSON.parse(decoded);
var profileId = payload.sub;
Logger.log('Profile ID: ' + profileId);
Method 2: use Firebase and getOAuthToken()
Steps to get Google ID Token from Apps Script's OAuth token:
Enable Identity Toolkit API for your Apps Script project.
Add new Firebase project to your existing Google Cloud Platform project at https://console.firebase.google.com/
Create Firebase app for platform: Web.
You will get your config data: var firebaseConfig = {apiKey: YOUR_KEY, ...}.
Enable Google sign-in method for your Firebase project at https://console.firebase.google.com/project/PROJECT_ID/authentication/providers.
Use Apps Script function to get ID Token for current user:
function getGoogleIDToken()
{
// get your configuration from Firebase web app's settings
var firebaseConfig = {
apiKey: "***",
authDomain: "*.firebaseapp.com",
databaseURL: "https://*.firebaseio.com",
projectId: "***",
storageBucket: "***.appspot.com",
messagingSenderId: "*****",
appId: "***:web:***"
};
var res = UrlFetchApp.fetch('https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key='+firebaseConfig.apiKey, {
method: 'POST',
payload: JSON.stringify({
requestUri: 'https://'+firebaseConfig.authDomain,
postBody: 'access_token='+ScriptApp.getOAuthToken()+'&providerId=google.com',
returnSecureToken: true,
returnIdpCredential: true
}),
contentType: 'application/json',
muteHttpExceptions: true
});
var responseData = JSON.parse(res);
idToken = responseData.idToken;
Logger.log('Google ID Token: ');
Logger.log(idToken);
return idToken;
}
Kudos to Riël Notermans

You should enable oAuth scopes,
https://developers.google.com/apps-script/concepts/scopes

Related

How to use UrlFetchApp.fetchAll with Calendar API in google workspace addon

Our Google Workspace Addon synchronises calendar events from Google Calendar to an external database.
As part of this process we update multiple Calendar Events using the patch command
Calendar.Events.patch({extendedProperties:pp},_e.calendar.calendarId,_e.calendar.id);
The problem we have is that we need to perform this operation multiple times within a limited amount of time (app script will time-out)
We could achieve this with the UrlFetchApp.fetchAll function but unfortunately to call this we would need to call the calendar api directly. Although this is easily done - we do not have the AccessToken as this is not available and is handled by the API.
Does anybody know how to get the accessToken (without pushing the user through a separate OAuth approval process) that is being used by the Calendar API so that we can utilise it to call UrlFetchApp.fetchAll
As long as you have the correct Calendar scope in your add-on, I think you can just pass ScriptApp.getOauthToken() as the token in the UrlFetchApp request:
getOAuthToken()
Gets the OAuth 2.0 access token for the effective user. If the script's OAuth scopes are sufficient to authorize another Google API that normally requires its own OAuth flow (like Google Picker), scripts can bypass the second authorization prompt by passing this token instead. The token expires after a time (a few minutes at minimum); scripts should handle authorization failures and call this method to obtain a fresh token when needed.
The token returned by this method only includes scopes that the script currently needs. Scopes that were previously authorized but are no longer used by the script are not included in the returned token. If additional OAuth scopes are needed beyond what the script itself requires, they can be specified in the script's manifest file.
Return
String — A string representation of the OAuth 2.0 token.
Example Request:
function patchCalendarEvents() {
const calendarId = "calendarId"
const eventIds = [] // your event ID list
const baseUrl = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events/{{eventId}}`
const requests = []
eventIds.forEach(function (event) {
requests.push({
"url": baseUrl.replace("{{eventId}}", event),
"method": "PATCH",
"headers": {
"Authorization": "Bearer " + ScriptApp.getOAuthToken()
},
"payload": '{"extendedProperties": {"shared": {"testKey": "Test Value"}}}',
"contentType": "application/json",
})
})
console.log(requests)
const responses = UrlFetchApp.fetchAll(requests)
responses.forEach(function(response) {
console.log(response.getContentText())
})
}

How to use Jobs resource in google apps script?

How do I use the 'Job' resource within the Google Apps Script?
I understand that YouTube Reporting API has to be enabled, but what object can it be called from?
I am trying to access YouTube Reporting API. is this the right way to call it?
If so, my Apps Script can't identify youtubeReporting, why would that be?
youtubeReporting.jobs()
To sum up, my goal is to extract 'Content Owner Reports' bulk/query, pretty much read all the steps leading to that, except the very starting point, which is an object name.
Note: Although this answer deals with YouTube reporting api, the flow should be the same for all Google apis, where advanced Google service wrapper isn't provided.
AFAICT, YouTube reporting API isn't directly available as a advanced Google service. You may be able to use YouTubeanalytics api, which is provided as a advanced Google service. To access reporting api, You need to directly connect to the api through HTTP calls and urlfetch.
Required reading:
Advanced or HTTP
ScriptApp#getOauthToken
YouTube reporting rest
UrlFetchApp#fetch
Editing manifest#Setting explicit scopes
Switch to standard GCP
API Library
Solution:
It is possible to access YouTube reporting api from Google apps script using UrlFetchApp
Full OAuth flow can be bypassed using oauth token provided by ScriptApp
Include scopes in the appsscript.json manifest file.
Switch to a standard GCP and enable the YouTube reporting api for this project. Otherwise 403 will be returned.
Snippet:
/**
* #description A wrapper for accessing Google apis from apps script
* #param {string} url Google URL endpoint
* #param {string} method HTTP method
* #param {object} requestBody Requestbody to send
* #param {object} pathParameters Parameters to modify in the url
* #param {object} queryParameters Queryparameters to append to the url
* #return {object} response
*/
function accessGoogleApiHTTP_(
url,
method,
requestBody = {},
pathParameters = {},
queryParameters = {}
) {
const modifiedUrl = Object.entries(pathParameters).reduce(
(acc, [key, value]) => acc.replace(key, value),
url
);
const queryUrl = Object.entries(queryParameters).reduce(
(acc, param) => acc + param.map(e => encodeURIComponent(e)).join('='),
'?'
);
const options = {
method,
contentType: 'application/json',
headers: {
Authorization: `Bearer ${ScriptApp.getOAuthToken()}` /*Need to set explicit scopes*/,
},
muteHttpExceptions: true,
payload: JSON.stringify(requestBody),
};
const res = UrlFetchApp.fetch(
modifiedUrl + queryUrl,
options
).getContentText();
console.log(res);
return JSON.parse(res);
}
/**
* #see https://developers.google.com/youtube/reporting/v1/reference/rest/v1/jobs/create
* #description POST https://youtubereporting.googleapis.com/v1/jobs
*/
function createYtReportingJob() {
const reportTypeId = 'id',
name = 'name';
const jsonRes = accessGoogleApiHTTP_(
'https://youtubereporting.googleapis.com/v1/jobs',
'POST',
{ reportTypeId, name },
undefined,
{ onBehalfOfContentOwner: 'contentOwnerId' }
);
}
Manifest scopes:
"oauthScopes": [
"https://www.googleapis.com/auth/yt-analytics",
"https://www.googleapis.com/auth/script.external_request"
]
Answer
In order to access the YouTube Reporting API you have to add YouTube Analytics API and optionally add the YouTube Data API as well. From this point you can create a report as in the Apps Script: YouTube Analytics example.
Bear in mind:
When importing the services like YouTube Analytics API be aware with the version.
You can specify the amount of metrics desired.
Reference
Apps Script: YouTube Analytics
YouTube Data API: Channel list
Analytics and Reporting APIs: Content Owner Reports

Does sending mail via nodemailer in firebase cloud functions require billing account?

I had deployed a firebase cloud function to send a welcome mail when a user signs in for the first time.
In the firebase console, in firebase cloud function log messages, I saw this error message when the function was invoked.
Error Message:
Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
Is it not possible to send emails for free using firebase cloud functions? if it is possible, please explain the procedure. (Possiblly with a sample code)
Edit 1:
1. I am currently using nodemailer for sending mail.
2. I am using Gmail as the mail service.
Does sending mail via nodemailer in firebase cloud functions require billing account?
NO, You DO NOT need a billing account to send email via nodmailer using cloud functions.
I was getting the billing error as yours in my cloud function. And I have done 2 simple steps and it's gone.
1. In your gmail account setting, enable Less secure app access to ON
2. Also go to this link and click continue https://accounts.google.com/DisplayUnlockCaptcha .
After doing the above 2 steps, the billing error is gone, and email is sending successfully from the cloud function.
And here is my nodejs code for your refernce:
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'xyzz#gmail.com',
pass: '123'
},
});
exports.sendMail = functions.https.onRequest(async (req, res) => {
const mailOptions = {
from: '"Test." <noreply#firebase.com>',
to: 'xyz#gmail.com'
};
// Building Email message.
mailOptions.subject = 'Thanks and Welcome!'
mailOptions.text = 'Thanks you for subscribing to our newsletter. You will receive our next weekly newsletter.'
try {
await mailTransport.sendMail(mailOptions);
console.log('subscription confirmation email sent to');
return res.send('Sended');
} catch (error) {
console.error('There was an error while sending the email:', error);
return res.send(error.toString());
}
});
You can test locally before you deploy it
firebase serve --only functions
you will get a link http://localhost:5000/project-name/us-central1/sendMail; paste it in the browser and the cloud function will run. If any errors it will show up in the browser and console/powershell

Stackdriver export to .txt or PDF on drive/mail

I've set up an script which reads data from a spreadsheet and sends emails according this data.
Now, I've also set it up to do some simple logging via stackdriver.
What I'd like to do is to export these logs (after/at the end of every execution of the mail-script) to a .txt or .pdf file which then get saved to a specific Google Drive folder or been send by mail.
Unfortunately I can't seem to find out how to do this, or if its even posible?
There is no way to edit a Google docs file if this is what you where thinking of doing. Your going to have to create your .txt or .pdf file locally then upload the file to Google drive or send it as an email. Technically if you upload the file as a .txt i think that Google drive will allow you to export it as pdf but i haven't tried with the new version of Google drive.
var fileId = '1ZdR3L3qP4Bkq8noWLJHSr_iBau0DNT4Kli4SxNc2YEo';
var dest = fs.createWriteStream('/tmp/resume.pdf');
drive.files.export({
fileId: fileId,
mimeType: 'application/pdf'
})
.on('end', function () {
console.log('Done');
})
.on('error', function (err) {
console.log('Error during download', err);
})
.pipe(dest);
Downloading google Documents
I also dont think that you will be able to email a file directly from Google Drive you will have to download the file locally then add send your email.
Stackdriver has an error reporting API. Documentation for Stackdriver The API has REST capability, which means that you can call it from Apps Script using UrlFetchApp.fetch(url) where url is the url needed to get error reporting information. The base url for the Stackdriver API is: https://clouderrorreporting.googleapis.com The API must be enabled.
There are multiple methods that can be used with the API.
The method that you probably need is the list method, which requires the url:
https://clouderrorreporting.googleapis.com/v1beta1/{projectName=projects/*}/events
where the projectName parameter must be a Google Cloud Platform project ID.
See documentation on list at: projects.events.list
The return value for that HTTPS Request, if successful, is a "response body" with the following structure and data:
{
"errorEvents": [
{
object (ErrorEvent)
}
],
"nextPageToken": string,
"timeRangeBegin": string
}
The ErrorEvent is a JSON object with the following structure and data:
{
"eventTime": string,
"serviceContext": {
object (ServiceContext)
},
"message": string,
"context": {
object (ErrorContext)
}
}
So, if you want to send an email with error data from Stackdriver, it won't be sent directly from Stackdriver, you need to make a request to Stackdriver from Apps Script, get the error information, and then send an email from Apps Script.
Of course, you could have your own error handling system, that logged error information to some external target, (Eg. your spreadsheet, or a database) using UrlFetchApp.fetch(url);
To make the request to the Stackdriver API you would need code something like this:
var projectID = "Enter project ID";
var url = 'https://clouderrorreporting.googleapis.com/v1beta1/' + projectID
+ '/events';
var tkn = ScriptApp.getOAuthToken();
var options = {};
options.headers = {Authorization: 'Bearer ' + tkn}
options.muteHttpExceptions = true;
var rtrnObj = UrlFetchApp.fetch(url,options);
Logger.log(rtrnObj.getContentText())
I haven't use this API and I haven't tested this code. If anyone uses it, and has information or finds an error, please make a comment.

"Insufficient Permission" when trying to authenticate to cloud-storage via apps-script

I am about to give up on this as I can't find out what I am doing wrong.
I have a cloud-storage bucket with our companies billing data (json file objects written by google) that I am supposed to process into spreadsheets.
As there is no apps script API for oauth2, I am using the custom OAuth2 library provided by google with the key "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", and have setup the auth request as shown in this example for service accounts:https://github.com/googlesamples/apps-script-oauth2/blob/master/samples/GoogleServiceAccount.gs
The token is being created and put into the scripts property store, where I can view it. So far so good.
I have this code for requesting the token and then I am trying to list the contents of the bucket in the function "getFilesList()":
function getService() {
return OAuth2.createService('CloudStoreGrab-Service')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(creds_private_key)
.setIssuer(creds_client_email)
.setSubject(creds_user_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setScope(['https://www.googleapis.com/auth/drive','https://www.googleapis.com/auth/script.external_request','https://www.googleapis.com/auth/script.storage','https://www.googleapis.com/auth/spreadsheets']);
}
function getFilesList() {
var service = getService();
service.reset();
if (service.hasAccess()) {
var url = 'https://www.googleapis.com/storage/v1/b/'+bucket+'/o';
var response = UrlFetchApp.fetch(url, {
method: "GET",
muteHttpExceptions: true,
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
}
Logger.log("Response:", response.getContentText())
}
No matter what I seem to try, the request always returns "403 Insufficient Permission". The service account has all necessary roles and permissions activated though (DwD, Storage-Administrator, Project-Owner). When I authenticate with the same credentials from gcloud and then browse the bucket with gsutils I can see the listing. This leads me to believe, that I am still requesting the auth token incorrectly. I tried using the generated token with curl and am getting the same Insufficient Permission response.
What am I doing wrong, while requesting the token?
Are the requested scopes too narrow?
Are the requested scopes too narrow?
That they are. You can find the OAuth scopes for Google's Cloud Storage API listed below (you won't need to use all of them, pick the ones best suited to your use-case, the 1st and 5th scopes in the list should be sufficient):
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/cloud-platform.read-only
https://www.googleapis.com/auth/devstorage.full_control
https://www.googleapis.com/auth/devstorage.read_only
https://www.googleapis.com/auth/devstorage.read_write
In future, you can find the required OAuth scopes for any Google API you need at the following link:
https://developers.google.com/identity/protocols/googlescopes