Search code examples
node.jsgoogle-sheets-apiservice-accountsgoogle-api-nodejs-clienttwilio-functions

How to use googleapis google.auth.GoogleAuth() for google API service account in Twilio serverless function?


How to use googleapis google.auth.GoogleAuth() for google API service account in Twilio serverless function, since there is no FS path to provide as a keyFile value?

Based on the example here ( https://www.section.io/engineering-education/google-sheets-api-in-nodejs/ ) and here ( Google api node.js client documentation ) my code is based on the example here ( Receive an inbound SMS ) and looks like...

const {google} = require('googleapis')
const fs = require('fs')
exports.handler = async function(context, event, callback) {
  const twiml = new Twilio.twiml.MessagingResponse()
  
  // console.log(Runtime.getAssets()["/gservicecreds.private.json"].path)
  console.log('Opening google API creds for examination...')
  const creds = JSON.parse(
    fs.readFileSync(Runtime.getAssets()["/gservicecreds.private.json"].path, "utf8")
  )
  console.log(creds)
  
  // connect to google sheet
  console.log("Getting googleapis connection...")
  const auth = new google.auth.GoogleAuth({
    keyFile: Runtime.getAssets()["/gservicecreds.private.json"].path,
    scopes: "https://www.googleapis.com/auth/spreadsheets",
  })
  const authClientObj = await auth.getClient()
  const sheets = google.sheets({version: 'v4', auth: authClientObj})
  const spreadsheetId = "myspreadsheetID"
  
  console.log("Processing message...")
  if (String(event.Body).trim().toLowerCase() == 'KEYWORD') {
    console.log('DO SOMETHING...')
    try {
      // see https://developers.google.com/sheets/api/guides/values#reading_a_single_range
      let response = await sheets.spreadsheets.values.get({
        spreadsheetId: spreadsheetId,
        range: "'My Sheet'!B2B1000"
      })
      console.log("Got data...")
      console.log(response)
      console.log(response.result)
      console.log(response.result.values)
    } catch (error) {
      console.log('An error occurred...')
      console.log(error)
      console.log(error.response)
      console.log(error.errors)
    }
  } 
  
  // Return the TwiML as the second argument to `callback`
  // This will render the response as XML in reply to the webhook request
  return callback(null, twiml)

...where the Asset referenced in the code is for a JSON generated from creating a key pair for a Google APIs Service Account and manually copy/pasting the JSON data as an Asset in the serverless function editor web UI.

I see error messages like...

An error occurred...

{ response: '[Object]', config: '[Object]', code: 403, errors: '[Object]' }

{ config: '[Object]', data: '[Object]', headers: '[Object]', status: 403, statusText: 'Forbidden', request: '[Object]' }

[ { message: 'The caller does not have permission', domain: 'global', reason: 'forbidden' } ]

I am assuming that this is due to the keyFile not being read in right at the auth const declaration (IDK how to do it since all the example I see assume a local filepath as the value, but IDK how to do have the function access that file for a serverless function (my attempt in the code block is really just a shot in the dark)).

FYI, I can see that the service account has an Editor role in the google APIs console (though I notice the "Resources this service account can access" has the error

"Could not fund an ancestor of the selected project where you have access to view a policy report on at least one ancestor"

(I really have no idea what that means or implies at all, very new to this)). Eg... enter image description here

Can anyone help with what could be going wrong here?

(BTW if there is something really dumb/obvious that I am missing (eg. a typo) just LMK in a comment so can delete this post (as it would then not serve any future value of others))


Solution

  • The caller does not have permission', domain: 'global', reason: 'forbidden

    This actually means that the currently authenticated user (the service account) does ot have access to do what you are asking it to do.

    You are trying to access a spread sheet.

    Is this sheet on the service accounts google drive account? If not did you share the sheet with the service account?

    The service account is just like any other user if it doesn't have access to something it cant access it. Go to the google drive web application and share the sheet with the service account like you would share it with any other user just use the service account email address i think its called client id its the one with an @ in it.

    delegate to user on your domain

    If you set up delegation properly then you can have the service account act as a user on your domain that does have access to the file.

    delegated_credentials = credentials.with_subject('userWithAccess@YourDomain.org')