Google Analytics - invalid_grant: Invalid JWT Signature - node.js

I need to authorize from Google analytics to get the response data.
var google = require('googleapis'),
q = require('q'),
SERVICE_ACCOUNT_EMAIL = '838823084353-cjjoiv9di67fuh7geqgggociibataf9v#developer.gserviceaccount.com',
SERVICE_ACCOUNT_KEY_FILE = __dirname + '/google-services-private-key.pem';
var def = q.defer();
var gAnalytics = google.analytics('v3');
var authClient = new google.auth.JWT( SERVICE_ACCOUNT_EMAIL, SERVICE_ACCOUNT_KEY_FILE, null, ['https://www.googleapis.com/auth/analytics.readonly']);
console.log(authClient)
authClient.authorize(function (err, tokens) {
if (err) {
console.log("err is: " + err, tokens);
return;
}
But it fails to authorize
getting error
JWT { transporter: DefaultTransporter {}, clientId_: undefined,
clientSecret_: undefined, redirectUri_: undefined, opts: {},
credentials: { refresh_token: 'jwt-placeholder', expiry_date: 1 },
email:
'838823084353-cjjoiv9di67fuh7geqgggociibataf9v#developer.gserviceaccount.com',
keyFile:
'/home/aaa/Desktop/ampretailer/server/google-services-private-key.pem',
key: null, scopes: [
'https://www.googleapis.com/auth/analytics.readonly' ], subject:
undefined, gToken: [Function: GoogleToken] } err is: Error:
invalid_grant: Invalid JWT Signature. { access_token: null,
token_type: 'Bearer', expiry_date:null }

I recommend you try using Google Analytics v4 instead of v3 there are a number of dimensions and metrics which you will not have access to using V3.
'use strict';
const { google } = require('googleapis');
const sampleClient = require('../sampleclient');
const analyticsreporting = google.analyticsreporting({
version: 'v4',
auth: sampleClient.oAuth2Client
});
async function runSample () {
const res = await analyticsreporting.reports.batchGet({
resource: {
reportRequests: [{
viewId: '65704806',
dateRanges: [
{
startDate: '2018-03-17',
endDate: '2018-03-24'
}, {
startDate: '14daysAgo',
endDate: '7daysAgo'
}
],
metrics: [
{
expression: 'ga:users'
}
]
}]
}
});
console.log(res.data);
return res.data;
}
// if invoked directly (not tests), authenticate and run the samples
if (module === require.main) {
const scopes = ['https://www.googleapis.com/auth/analytics'];
sampleClient.authenticate(scopes)
.then(c => runSample())
.catch(e => console.error);
}
// export functions for testing purposes
module.exports = {
runSample,
client: sampleClient.oAuth2Client
};
Code ripped from analyticsReporting/batchGet.js
Service account - To use the service account based samples, create a new service account in the cloud developer console, and save the file as jwt.keys.json in the samples directory.
'use strict';
const {google} = require('googleapis');
const path = require('path');
/**
* The JWT authorization is ideal for performing server-to-server
* communication without asking for user consent.
*
* Suggested reading for Admin SDK users using service accounts:
* https://developers.google.com/admin-sdk/directory/v1/guides/delegation
*
* See the defaultauth.js sample for an alternate way of fetching compute credentials.
*/
async function runSample () {
// Create a new JWT client using the key file downloaded from the Google Developer Console
const client = await google.auth.getClient({
keyFile: path.join(__dirname, 'jwt.keys.json'),
scopes: 'https://www.googleapis.com/auth/analytics.readonly'
});
// Obtain a new drive client, making sure you pass along the auth client
const analyticsreporting = google.analyticsreporting({
version: 'v4',
auth: client
});
// Make an authorized request to list Drive files.
const res = = await analyticsreporting.reports.batchGet({
resource: {
reportRequests: [{
viewId: '65704806',
dateRanges: [
{
startDate: '2018-03-17',
endDate: '2018-03-24'
}, {
startDate: '14daysAgo',
endDate: '7daysAgo'
}
],
metrics: [
{
expression: 'ga:users'
}
]
}]
}
});
console.log(res.data);
return res.data;
}
if (module === require.main) {
runSample().catch(console.error);
}
// Exports for unit testing purposes
module.exports = { runSample };
code ripped from samples/jwt.js

Related

Trying to get user email storage from google admin api

I am building a little script, that access the Google Reseller api that gets all the domains, then finds the users in those domains, to which it then looks at the emails for those users. That part all works.
The issue is that after I have gotten the users emails, I need to check the email storage so that I can then send an alert once the storage is nearly full.
I have searched the internet and tried various different methods, but seem to have hit a roadblock with the final step.
This is as far a I got with code
let privatekey = require("./spartan-concord-344213-d46691ffcd02.json");
let spreadsheetPriKey = require("./spreadsheet_service_account.json");
const {JWT} = require('google-auth-library');
// configure a JWT auth client
let jwtClient = new JWT({
email: privatekey.client_email,
key: privatekey.private_key,
subject: 'subject#email.com',
scopes:['https://www.googleapis.com/auth/apps.order',
'https://www.googleapis.com/auth/admin.reports.audit.readonly',
'https://www.googleapis.com/auth/admin.reports.usage.readonly',
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.readonly',
],
});
let jwtSpreadsheetClient = new JWT({
email: spreadsheetPriKey.client_email,
key: spreadsheetPriKey.private_key,
scopes:['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive','https://www.googleapis.com/auth/spreadsheets.readonly'],
});
const sites = []
//authenticate request
jwtSpreadsheetClient.authorize(async function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
google.options({ auth: jwtSpreadsheetClient });
const client = google.sheets({ version: "v4" });
const supportEmail = await client.spreadsheets.values.get({ spreadsheetId: 'ID', range:'Sites!C2:C',});
let suppEmails = supportEmail.data.values;
suppEmails.forEach(element => {
if(element[0] != null){
sites.push(element[0]);
}
});
}
});
//authenticate request
jwtClient.authorize(async function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
google.options({ auth: jwtClient });
const service = google.reseller({version: 'v1'});
const res = await service.customers.get({
'customerId': 'testDomain',
});
// console.log(res);
const services = google.admin({version: 'directory_v1'});
respo = await services.users.get({
userKey: 'test#email.com',
projection: 'FULL',
viewType: 'admin_view'
});
// console.log(respo);
const servicess = google.admin({version: 'reports_v1'});
// const response = await servicess.userUsageReport.get({
// userKey: 'all',
// date: '2022-10-11',
// parameters: 'gmail:is_gmail_enabled',
// customerId: 'customerId'
// });
const response = await servicess.activities.list({
userKey: 'all',
applicationName: 'drive',
maxResults: 10,
customerId: 'customerId'
});
console.log(response.data);
}
});
Any information is much appreciated :)
It appears you can get this information from the Reports API via userUsageReport.get with a delay.
Which provides the following useful Account Parameters with applicationName=accounts.
drive_used_quota_in_mb
gmail_used_quota_in_mb
gplus_photos_used_quota_in_mb
total_quota_in_mb
used_quota_in_mb
used_quota_in_percentage
Found here.
const { JWT } = require('google-auth-library');
const keys = require('./jwt.keys.json');
async function main() {
const client = new JWT({
email: keys.client_email,
key: keys.private_key,
scopes: [
'https://www.googleapis.com/auth/admin.reports.audit.readonly',
'https://www.googleapis.com/auth/admin.reports.usage.readonly'
],
});
const userKey = 'example#gmail.com';
const threeDaysAgo = new Date();
threeDaysAgo.setDate(new Date().getDate() - 3);
const date = datethreeDaysAgo.toISOString().split('T').slice(0, 1)[0]
const url = `https://admin.googleapis.com/admin/reports/v1/usage/users/${userKey}/dates/${date}?parameters=accounts:used_quota_in_percentage,accounts:total_quota_in_mb,accounts:used_quota_in_mb,accounts:drive_used_quota_in_mb,accounts:gmail_used_quota_in_mb,accounts:gplus_photos_used_quota_in_mb`;
const res = await client.request({ url });
console.log(res.data);
/*
{
"kind": "admin#reports#usageReports",
"etag": "\"iOcWZfmq3FoTN4bnM3qjxTbCKtrhwribSW4KdcAqWMQ/jldXIL3cW34P49N2Row7rqWkykQ\"",
"usageReports": [
{
"kind": "admin#reports#usageReport",
"date": "2022-10-17",
"etag": "\"iOcWZfmq3FoTN4bnM3qjxTbCKtrhwribSW4KdcAqWMQ/vroqtrwJo5ZJIWScePvwODh5ZGI\"",
"entity": {
"type": "USER",
"customerId": "FVdfugb32",
"userEmail": "example#gmail.com",
"profileId": "2350766532076257332587"
},
"parameters": [
{
"name": "accounts:used_quota_in_percentage",
"intValue": "0"
},
{
"name": "accounts:total_quota_in_mb",
"intValue": "-1"
},
{
"name": "accounts:used_quota_in_mb",
"intValue": "858"
},
{
"name": "accounts:drive_used_quota_in_mb",
"intValue": "461"
},
{
"name": "accounts:gmail_used_quota_in_mb",
"intValue": "397"
},
{
"name": "accounts:gplus_photos_used_quota_in_mb",
"intValue": "0"
}
]
}
]
}
*/
}
main().catch(console.error);

How to attach a Google Meet link to a Calendar Event (created with a Service Account)?

I am trying to create a simple API call to create a Google Calendar Event with a Google Meet link in it but it seems I am unable to do so.
I looked up the Calendar API Documentation and have seen various examples but it still doesn't work for me. I am using a Service Account on NodeJS and a React frontend. Here below is the source code of my project.
const { google } = require('googleapis');
const { GoogleAuth } = require('google-auth-library');
var express = require('express');
var router = express.Router();
const SCOPES = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.addons.execute', 'https://www.googleapis.com/auth/calendar.settings.readonly', 'https://www.googleapis.com/auth/calendar.events'];
const GOOGLE_PRIVATE_KEY = "MY_PRIVATE_KEY"
const GOOGLE_CLIENT_EMAIL = "MY_SERVICE_ACCOUNT"
const GOOGLE_PROJECT_NUMBER = "MY_PROJECT_NUMBER"
const GOOGLE_CALENDAR_ID = "MY_CALENDAR_ID"
const jwtClient = new google.auth.JWT(
GOOGLE_CLIENT_EMAIL,
null,
GOOGLE_PRIVATE_KEY,
SCOPES,
"MY_PERSONAL_EMAIL"
);
const calendar = google.calendar({
version: 'v3',
project: GOOGLE_PROJECT_NUMBER,
auth: jwtClient
});
const auth = new GoogleAuth({
keyFile: 'credentials.json',
scopes: 'https://www.googleapis.com/auth/calendar', //full access to edit calendar
});
auth.getClient();
router.get("/demo", (req, res) => {
var event = {
'summary': 'My first event!',
'location': 'Hyderabad,India',
'description': 'First event with nodeJS!',
'start': {
'dateTime': '2022-06-28T09:00:00-07:00',
'timeZone': 'Asia/Dhaka',
},
'end': {
'dateTime': '2022-06-29T17:00:00-07:00',
'timeZone': 'Asia/Dhaka',
},
'attendees': [],
'reminders': {
'useDefault': false,
'overrides': [
{ 'method': 'email', 'minutes': 24 * 60 },
{ 'method': 'popup', 'minutes': 10 },
],
},
"conferenceData": {
'createRequest': {
"requestId": getRandomString(),
"conferenceSolution": {
"key": {
"type": "hangoutsMeet",
}
},
}
}
};
calendar.events.insert({
auth: auth,
calendarId: GOOGLE_CALENDAR_ID,
requestBody: event,
conferenceDataVersion: 1,
}, function (err, event) {
if (err) {
console.log('There was an error contacting the Calendar service: ' + err);
return;
}
console.log('Event created: %s', event.data);
res.jsonp("Event successfully created!");
});
})
Did you solve the Issue?
I also looked into this part because of a side project, and I'll share the code I've had success with.
I'm not sure which part is different when I look at it.
I hope this code helps.
/* index.js */
const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const {authenticate} = require('#google-cloud/local-auth');
const {google} = require('googleapis');
const { v4: uuidv4 } = require('uuid');
const express = require('express');
const app = express()
// OAuth scope
const SCOPES = ['https://www.googleapis.com/auth/calendar.events'];
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');
/* Reads previously authorized credentials from the save file. */
async function loadSavedCredentialsIfExist() {
try {
const content = await fs.readFile(TOKEN_PATH);
const credentials = JSON.parse(content);
return google.auth.fromJSON(credentials);
} catch (err) {
return null;
}
}
/* Serializes credentials to a file compatible with GoogleAUth.fromJSON. */
async function saveCredentials(client) {
const content = await fs.readFile(CREDENTIALS_PATH);
const keys = JSON.parse(content);
const key = keys.installed || keys.web;
const payload = JSON.stringify({
type: 'authorized_user',
client_id: key.client_id,
client_secret: key.client_secret,
refresh_token: client.credentials.refresh_token,
});
await fs.writeFile(TOKEN_PATH, payload);
}
/* Load or request or authorization to call APIs. */
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH,
});
if (client.credentials) {
await saveCredentials(client);
}
return client;
}
/**
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
async function inserEvent(auth) {
const calendar = google.calendar({version: 'v3', auth});
const event = {
summary: 'Demo',
description: 'Demo for Google Meet',
start: {
dateTime: '2022-11-20T09:00:00+09:00',
timeZone: 'Asia/Seoul',
},
end: {
dateTime: '2022-11-20T09:30:00+09:00',
timeZone: 'Asia/Seoul',
},
conferenceData: {
createRequest: {
conferenceSolutionKey: {type: 'hangoutsMeet'},
requestId: uuidv4(),
},
},
attendees: [
{email: '${email1}'}, /* insert email */
{email: '${email2}'}, /* insert email */
],
reminders: {
useDefault: false,
overrides: [
{method: 'email', minutes: 60},
{method: 'popup', minutes: 10},
],
},
};
calendar.events.insert({
auth: auth,
calendarId: 'primary',
resource: event,
conferenceDataVersion: 1,
}, function(err, event) {
if (err) {
console.log('There was an error contacting the Calendar service: ' + err);
return;
}
console.log('Event created, google meet link : %s', event.data.hangoutLink);
});
}
app.get("/demo", (rqe, res) => {
authorize().then(inserEvent).catch(console.error);
res.send('Good');
})
app.listen(3000, () => {
console.log('listening on port 3000');
})

How to test cloud functions locally with same authorizations as remote

I've been trying out cloud functions for a little while now. Recently, I found out about functions-framework. It basically allows you to run your functions locally. This helps/should help in reducing the time it takes to test your code.
I am running into an issue where calling the functions locally - curl localhost:8080 outputs Insufficient Permission: Request had insufficient authentication scopes while testing them via the console produces the expected result.
I am trying to move data from google drive to google cloud storage
const { google } = require("googleapis");
const { google } = require("googleapis");
const SCOPES = [
"profile",
"email",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive.metadata.readonly",
"https://www.googleapis.com/auth/drive.appdata",
"https://www.googleapis.com/auth/drive.metadata",
];
async function addDocument(authClient, bucket, data) {
const version = "v1";
const storage = google.storage({ version, auth: authClient });
const response = await storage.objects.insert({
bucket,
requestBody: {
name: data.name,
},
media: {
body: data.content,
mimeType: data.mimeType,
},
});
return response.data;
}
async function getDocumentContent(authClient, fileId) {
const version = "v3";
const drive = google.drive({ version, client: authClient });
const json = await drive.files.get({
alt: "media",
fileId: fileId,
auth: authClient,
});
const content = await drive.files.get({
alt: "media",
fileId: fileId,
auth: authClient,
});
const data = {
content: content.data,
mimeType: json.data.mimeType,
name: json.data.name,
};
return data;
}
async function getDocuments(authClient, folderId) {
const query = `parents= '${folderId}'`;
const fields = "files(id, name)";
const version = "v3";
const drive = google.drive({ version, client: authClient });
const response = await drive.files.list({
q: query,
fields,
auth: authClient,
});
const { files } = response.data;
return files;
}
exports.copy = async (req, res) => {
const auth = new google.auth.GoogleAuth({
scopes: SCOPES,
});
const client = await auth.getClient();
const containingFolder = "folder_id";
const bucketName = "some_bucket";
try {
const files = await getDocuments(client, containingFolder);
res.send(files);
} catch (err) {
res.send(err.message);
}
};
My question here is, how do I get local calls to have the same authentication as my remote calls?
I wrote an article on that. At the time where I wrote the article, only Java and Go (because I contributed to implement the feature in the client library) were compliant.
Have a try with NodeJS, it might be implemented now.

Service account : Google Analytics v4 : Error User does not have any Google Analytics account

I'm trying to add function in firebase to get the analysis data of users.
code: 403,
> errors: [
> {
> message: 'User does not have any Google Analytics account.',
> domain: 'global',
> reason: 'forbidden'
> }
> ]
I'm getting this error when I run it to the command prompt. Not sure where I'm doing mistakes. I got the access token but the error was generated by the reports.batchGet code.
import * as functions from 'firebase-functions';
const gClientEmail = functions.config().google.client_email
const gPrivateKey = functions.config().google.private_key.replace(/\\n/g, '\n')
const { google } = require('googleapis');
const jwtClient = new google.auth.JWT(gClientEmail, null, gPrivateKey, 'https://www.googleapis.com/auth/analytics.readonly', '');
const oauth2Client = new google.auth.OAuth2();
google.options({ auth: oauth2Client });
exports.init = functions.https.onRequest((request, response) => {
jwtClient.authorize((err:any, tokens:any) => {
if (err) {
throw (err);
}
oauth2Client.setCredentials({
access_token: tokens.access_token
});
var analytics = google.analyticsreporting('v4');
var req = {
reportRequests: [
{
viewId: VIEWID,
dateRanges: [
{
startDate: "10daysAgo",
endDate: "today",
},
],
metrics: [
{
expression: "ga:users",
},
],
dimensions: [
{
name: "ga:date",
},
],
}
],
};
analytics.reports.batchGet({
'auth': jwtClient,
'resource': req,
},
function(err:any, response:any) {
if (err) {
console.log(err);
return;
}
console.log(response);
}
);
});
});
I got one solution in which they have said to add the gClientEmail to each site's user in analytics dashboard. I don’t want to do this.
If you have only Google Analytics 4 Property in your account, you can't use Google Analytics Reporting API because work only with Universal Analytics Property.
API v4 is not API for GA4.
For GA4 API you have to use Analytics Data API: https://developers.google.com/analytics/devguides/reporting/data/v1

Domain-wide delegation using default credentials in Google Cloud Run

I'm using a custom service account (using --service-account parameter in the deploy command). That service account has domain-wide delegation enabled and it's installed in the G Apps Admin panel.
I tried this code:
app.get('/test', async (req, res) => {
const auth = new google.auth.GoogleAuth()
const gmailClient = google.gmail({ version: 'v1' })
const { data } = await gmailClient.users.labels.list({ auth, userId: 'user#domain.com' })
return res.json(data).end()
})
It works if I run it on my machine (having the GOOGLE_APPLICATION_CREDENTIALS env var setted to the path of the same service account that is assigned to the Cloud Run service) but when it's running in Cloud Run, I get this response:
{
"code" : 400,
"errors" : [ {
"domain" : "global",
"message" : "Bad Request",
"reason" : "failedPrecondition"
} ],
"message" : "Bad Request"
}
I saw this solution for this same issue, but it's for Python and I don't know how to replicate that behaviour with the Node library.
After some days of research, I finally got a working solution (porting the Python implementation):
async function getGoogleCredentials(subject: string, scopes: string[]): Promise<JWT | OAuth2Client> {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
})
const authClient = await auth.getClient()
if (authClient instanceof JWT) {
return (await new google.auth.GoogleAuth({ scopes, clientOptions: { subject } }).getClient()) as JWT
} else if (authClient instanceof Compute) {
const serviceAccountEmail = (await auth.getCredentials()).client_email
const unpaddedB64encode = (input: string) =>
Buffer.from(input)
.toString('base64')
.replace(/=*$/, '')
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://accounts.google.com/o/oauth2/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: subject,
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT',
})
const iamPayload = `${unpaddedB64encode(header)}.${unpaddedB64encode(payload)}`
const iam = google.iam('v1')
const { data } = await iam.projects.serviceAccounts.signBlob({
auth: authClient,
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
requestBody: {
bytesToSign: unpaddedB64encode(iamPayload),
},
})
const assertion = `${iamPayload}.${data.signature!.replace(/=*$/, '')}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({ assertion, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer' })
const response = await fetch('https://accounts.google.com/o/oauth2/token', { method: 'POST', headers, body }).then(r => r.json())
const newCredentials = new OAuth2Client()
newCredentials.setCredentials({ access_token: response.access_token })
return newCredentials
} else {
throw new Error('Unexpected authentication type')
}
}
What you can do here is define ENV variables in your yaml file as described in this documentation to set the GOOGLE_APPLICATION_CREDENTIALS to the path of the JSON key.
Then use a code such as the one mentioned here.
const authCloudExplicit = async ({projectId, keyFilename}) => {
// [START auth_cloud_explicit]
// Imports the Google Cloud client library.
const {Storage} = require('#google-cloud/storage');
// Instantiates a client. Explicitly use service account credentials by
// specifying the private key file. All clients in google-cloud-node have this
// helper, see https://github.com/GoogleCloudPlatform/google-cloud-node/blob/master/docs/authentication.md
// const projectId = 'project-id'
// const keyFilename = '/path/to/keyfile.json'
const storage = new Storage({projectId, keyFilename});
// Makes an authenticated API request.
try {
const [buckets] = await storage.getBuckets();
console.log('Buckets:');
buckets.forEach(bucket => {
console.log(bucket.name);
});
} catch (err) {
console.error('ERROR:', err);
}
// [END auth_cloud_explicit]
};
Or follow an approach similar to the one mentioned here.
'use strict';
const {auth, Compute} = require('google-auth-library');
async function main() {
const client = new Compute({
serviceAccountEmail: 'some-service-account#example.com',
});
const projectId = await auth.getProjectId();
const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({url});
console.log(res.data);
}
main().catch(console.error);

Resources