Node.js Google Sheets API: can read but not create spreadsheet - node.js

I have been working all day to try to get a Node.js application connected to my Google Drive to programmatically create spreadsheets using the Google Sheets API.
I think I have set up my connection correctly, because the following code block executes correctly:
/**
* 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;
}
/**
* Prints the names and majors of students in a sample spreadsheet:
* #see https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
* #param {google.auth.OAuth2} auth The authenticated Google OAuth client.
*/
async function listMajors(auth) {
const sheets = google.sheets({ version: 'v4', auth });
const res = await sheets.spreadsheets.values.get({
spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
range: 'Class Data!A2:E',
});
const rows = res.data.values;
if (!rows || rows.length === 0) {
console.log('No data found.');
return;
}
console.log('Name, Major:');
rows.forEach((row) => {
// Print columns A and E, which correspond to indices 0 and 4.
console.log(`${row[0]}, ${row[4]}`);
});
}
authorize().then(listMajors).catch(console.error);
As soon as I move from that code to the following code, I get 403 Insufficient Permission errors:
/**
* Create a google spreadsheet
* #param {string} title Spreadsheets title
* #return {string} Created spreadsheets ID
*/
async function create(title) {
const { GoogleAuth } = require('google-auth-library');
const { google } = require('googleapis');
const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/spreadsheets',
});
const service = google.sheets({ version: 'v4', auth });
const resource = {
properties: {
title,
},
};
try {
const spreadsheet = await service.spreadsheets.create({
resource,
fields: 'spreadsheetId',
});
console.log(`Spreadsheet ID: ${spreadsheet.data.spreadsheetId}`);
return spreadsheet.data.spreadsheetId;
} catch (err) {
// TODO (developer) - Handle exception
throw err;
}
}
authorize().then(create).catch(console.error);
I have tried this using OAuth2 Client ID, Service Account, and Application Default Credentials. I have enabled all Scopes for my application. I really don't understand what else it wants me to configure to tell it I can have access.
I am attempting to access an application created using my personal Gmail address by logging in using my personal Gmail address. When trying to use a Service Account, I created a new folder in Drive and gave it permissions.
I do not see any additional info in the Create a spreadsheet tutorial.
What other permission is it expecting me to grant and where?

In my case, the application of my scopes took quite a bit longer than I anticipated. Once they were added to my application, the code worked correctly. I would estimate that it took 4-5 hours for the updates to finally show up.

Related

In 2022, Is It Still Possible to Use Google Sheets API with a Service Account?

The Google documentation on their Nodejs Quickstart is not great and it only shows an example of authenticating with an OAuth credential. I have a working, functional Discord bot that writes to my personal Google Sheet. However, because it's unverified by Google, my refresh token expires every 7 days and my bot will just stop working without warning until I manually go back in and give the user consent again. This is a pain, obviously.
From what I understand, a Service Account wouldn't have this issue, however, I've been unable to find a working example of the authorize function when using a Service Account JSON key. For the OAuth key, I have this:
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.GoogleAuth(client_id, client_secret);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return getNewToken(oAuth2Client, callback);
oAuth2Client.setCredentials(JSON.parse(token));
return callback(oAuth2Client, arguments[2], arguments[3]);
});
}
The extra "arguments" parameters are just values I'm passing to the callback. Does anyone have any working examples of using the current API with a Service Account in Node? Thanks!
At least with the version 3.1.15 of google-spreadsheet that I used to work with Google Spreadsheet API I did this in order to connect using a service account (the example will be in typescript):
import { GoogleSpreadsheet } from 'google-spreadsheet';
async function getDocument(sheetId: string): Promise<GoogleSpreadsheet> {
const doc = new GoogleSpreadsheet(sheetId);
await doc.useServiceAccountAuth({
client_email: serviceEmail,
private_key: servicePrivateKey.replace(/\\n/g, '\n'),
})
// now we can use `doc`
return doc;
}
Some usages:
const doc = await getSpreadsheet(documentId);
await doc.loadInfo();
const sheet = doc.sheetsByTitle[sheetName];
const totalRowCount = sheet.rowCount;
...
await sheet.loadCells({
startRowIndex: 1,
endRowIndex: 100,
startColumnIndex: 0,
endColumnIndex: 10
});
const cell = sheet.getCell(rowIndex, columnIndex);

google sheets api connection - error JWT cannot be invoked without 'new'

i try to set connection to google sheets api with this code:
const { google } = require("googleapis");
const keys = require("./keys.json");
const client = google.auth.JWT(keys.client_email, null, keys.private_key, [
"https://www.googleapis.com/auth/spreadsheets",
]);
client.authorized(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("connected");
}
});
i get this error when run on vscode:
TypeError: Class constructor JWT cannot be invoked without 'new'
thank you
Modification points:
In this case, please add new like new google.auth.JWT(). This is from your error message.
client has not method of authorized(). Please modify to authorize().
When above points are reflected to your script, it becomes as follows.
Modified formula:
const { google } = require("googleapis");
const keys = require("./keys.json");
const client = new google.auth.JWT(keys.client_email, null, keys.private_key, [
"https://www.googleapis.com/auth/spreadsheets",
]);
client.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("connected");
}
});
const sheets = google.sheets({ version: "v4", auth: client }); // You can use the methods of Sheets API by this "sheets".
Note:
In this modification, it supposes that your keys.json is the file including the valid credential values of the service account. Please be careful this.
As the additional information, when you want to access to the Google Spreadsheet of your Google Drive, please share your Google Spreadsheet with the email of the service account. By this, you can access to it.

NodeJS Googleapis Service Account authentication

I'm trying to perform authentication on GoogleAPIs using a Service Account. I have a service account set up, with its credentials located at credentials.json. I try to access a private sheet, to which I added the E-Mail address of the service account with editing rights.
Here the code I am using:
const {
google
} = require('googleapis');
const fs = require('fs');
let scopes = ['https://www.googleapis.com/auth/spreadsheets'];
let credentials = require("./credentials.json");
const authClient = new google.auth.JWT(
credentials.client_email,
null,
credentials.private_key,
scopes);
authClient.authorize(function(err, tokens) {
if (err) {
console.log(err);
return;
} else {
authClient.setCredentials(tokens);
}
});
const sheets = google.sheets({
version: 'v4',
authClient
});
let spreadsheetId = //...
let range = //...
const request = {
spreadsheetId: spreadsheetId,
range: range
};
sheets.spreadsheets.values.get(request, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
} else {
console.log('Result: ' + response);
}
});
I guess the API changed over time, since many guides showed different approaches, and in the end none worked for me.
The error is as follows:
The API returned an error: Error: The request is missing a valid API key.
To my understanding, a simple API key should only be necessary for unauthenticated access on public sheets, so I don't get why it is even requiring that. If I add such an API key I get the error
The API returned an error: Error: The caller does not have permission
Using
$ npm list googleapis
`-- googleapis#52.1.0
Any help would be greatly appreciated.
For who still facing googleapis problems within NodeJS Runtime in 2022.
Firstly, redirect into Google-IAM-Admin/ServiceAccount to pick the current working project.
Secondly, click to jump into Service Account that has the following format project#sub-name-id.iam.gserviceaccount.com.
Thirdly, between [Details, Permissions, Keys, Metrics, Logs]. Jump into Keys then Add Key -> Create new Key -> Key type::JSON and save JSON file to your computer.
Here within NodeJS Runtime, I use the following Semantic Version
googleapis#100.0.0
You can create JWT Client and inject into google default auth at google.options({auth: client}); or provide auth-client to specific Service as google.chat({version: 'v1', auth: client});
However, in the following example. I create a GoogleAuth instance and then make an AuthClient after. Which resulted the same behaviour to the JWT Method.
/** Import Node Native Dependencies !*/
import * as path from "path";
/** Import ES6 Default Dependencies !*/
import {google} from "googleapis";
const {client_email, private_key} = require('$/keys/credentials.json');
/**
** #description - Google [[Service Account]] Authenticator.
**/
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve('keys/credentials.json'),
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
const client = new google.auth.JWT({
email: client_email,
key: private_key,
/** Scopes can be specified either as an array or as a single, space-delimited string; ~!*/
scopes: [
"https://www.googleapis.com/auth/chat.bot",
],
});
(async () => {
/** #description - Either [[Get Client]] from [Google Auth] or Use directly from [JWT Client] ~!*/
const client = await auth.getClient();
/** #description - Use this Authorized Client as Default Authenticated to fallback from [Non-Authenticated Services] ~!*/
google.options({auth: client});
const chat = google.chat({
version: 'v1',
/** #description - Provide [Authenticated Services] to [Google Chat Service] Instance ~!*/
auth: client,
});
const response = await chat.spaces.members.get({
// Required. Resource name of the attachment, in the form "spaces/x/messages/x/attachments/x".
name: 'spaces',
});
console.log('response', response.data);
return void 0;
})();

Google Directory API: Unable to access User/Group endpoints using Service Account (403)

I am trying to verify members of groups using the Google Directory API and cannot get past a 403 error every time I make the request.
I am using a service account, which I have enabled the "Enable G Suite Domain-wide Delegation" option for. I have also added the "https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.group" Scopes using the Client ID within Suite under, "Manage API Client Access"
Code wise, I am using Node for this, and the google supplied googleapis package from NPM.
The external JSON file is the JSON credentials file downloaded when I created the service user.
Here's the code of me trying to get the request.
import { google } from 'googleapis';
async function getGroupUsers(){
const auth = await google.auth.getClient({
keyFile: './src/jwt.keys.json',
scopes: [
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.group.member',
],
});
const admin = google.admin({
version: 'directory_v1',
auth,
});
const res = await admin.groups.get({
groupKey: 'redacted#domain.redacted',
});
console.log(res)
}
I can't see any obvious reason this isn't working, as I can't see how the user doesn't have permission to the resource?
Obviously missing something obvious here, as the google documentation for this is all over the shop sadly.
Help greatly appreciated!
Thanks
Gareth
Ok after much banging of head and googling I finally for there with this, final working code is as follows, not the inclusion of the client.subject value, which has to be an administrator for the domain in question.
async function validateToken(idToken) {
const keys = JSON.parse(GOOGLE_CREDS);
const client = auth.fromJSON(keys);
client.scopes = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
];
client.subject = 'admin#gsuite.domain';
const admin = google.admin({
version: 'directory_v1',
// auth,
auth: client,
});
const res = await admin.groups.list({
domain: 'redacted',
userKey: email,
});
const { groups } = res.data;
let role = '';
// Check for user role
if (containsGroup(USER_GROUP, groups)) {
role = USER_GROUP;
}
// Check for admin role
if (containsGroup(ADMIN_GROUP, groups)) {
role = ADMIN_GROUP;
}
// Not an admin or user so return unathenticated
if (role === '') {
return authResponse();
}
return successResponse({
'X-Hasura-User-Id': userid,
'X-Hasura-Email': email,
'X-Hasura-Role': role,
'X-Hasura-Groups': groups.map(group => group.id),
'Cache-Control': 'max-age=600',
});
}

Error 'Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup' on API call

Trying to make my expressJS app control users' google drive files. I've been utilizing the node 'googleapis' package. After following a slightly dated/incorrect article here, I've gotten the script to:
redirect a user to their authorization url
grab the 'code' from get parameter and then...
register it back as access tokens, which can then be used to...
create a registered 'auth' object
When I use this to create the drive object and try to get it to list files, I get the following error: 'Error: Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup'
This error has already popped up on SO question, and on github.
Following general advice, I re-enabled the drive api, re-downloaded access key. I've also tried replacing the sensitive drive scope a gmail scope, but that didn't work either. I'm not sure where else turn to start debugging at this point. I have a sneaking suspicion my entire auth object is being formed incorrectly but I can't find anything wrong.
This is the related piece of Express app code I'm using to create the authObject and then read drive files.
/**
* Google Utility class that packages different actions related to authentication
*/
class GoogleUtil {
constructor(secretFileName = 'client_secret.json') {
const secret = JSON.parse(fs.readFileSync(secretFileName)).web;
const { client_id, client_secret, redirect_uris } = secret;
this.client_id = client_id;
this.client_secret = client_secret;
this.redirect_uri = redirect_uris[0];
this.standardScope = [
'https://www.googleapis.com/auth/drive',
// 'https://www.googleapis.com/auth/gmail.readonly',
// 'https://www.googleapis.com/auth/userinfo.profile'
];
}
createConnection() {
return new google.auth.OAuth2(this.client_id, this.client_secret, this.redirect_uri); // form authObject
}
getConnectionUrl(auth) {
return auth.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: this.standardScope
});
}
async getAccessTokenFromCode(code) {
var auth = this.createConnection();
const data = await auth.getToken(code);
return data.tokens;
}
}
const g = new GoogleUtil();
/**
* BEGIN ROUTE DEFINITIONS
*/
// THIS IS FIRST STEP. FRONTEND WILL REDIRECT TO GIVEN URL
app.get('/api/googleLoginUrl', async (req, res) => {
const oAuth2Client = g.createConnection();
const url = g.getConnectionUrl(oAuth2Client);
res.json({ url });
});
// *****
// NOTE: THIS IS ROUTE THAT ATTEMPTS TO LIST FILES AND THROWS ERROR
// *****
app.get('/google-auth-redirect', async (req, res) => {
if (!req.query.code) return res.send('Malformed request');
const tokens = await g.getAccessTokenFromCode(req.query.code);
const auth = g.createConnection().setCredentials(tokens);
const drive = google.drive({ version: 'v3', auth: auth });
drive.files.list({
pageSize: 10,
fields: 'nextPageToken, files(id, name)',
}, (err, resp) => {
if (err) return console.log('The API returned an error: ' + err);
console.log(resp);
});
res.redirect('/');
});
In the google developer console, clicking on 'create credentials' in the drive API overview informs me that my current credentials are compatible. The project scopes do include ' ../auth/drive'.
I'd want it to be able to list files from an authenticated user's account.
I think this might be related to how you are asking for permissions. If you are using your application to manipulate user's drive files you need a couple of things:
Check you have the correct access scopes setup.
Check you authentication parameter/Oauth screen is setup with said scopes.
You might want to read some documentation regarding authorizing users
Hope this helps! ;)

Resources