Oauth Causing More permissions problems - google-apps-script

I've been working to solve this issue for months and it is just getting worse.
I have a simple program in Google Apps script that grabs email addresses from a sheet and puts them in the appropriate Google Contacts group. It was working fine up until a month or so ago when it stopped working.
Please help me:
1) Understand If I really need to work with API's to do what I'm doing.
2) Check my work below for something I might be missing.
Original Problem:
When I run this code i get the error: "You do not have permission to perform that action."
function testAddEmail(){
ContactsApp.createContact(null, null, "tomfoolery#validemailaddress.com");
};
To resolve this issues I've:
-Made sure Contacts API is enabled in my Cloud Console.
-Disabled and reinabled it multiple times.
-Created a new file with the program and enabled new Contacts API.
-Created Credentials for the new program (It worked for one day after this).
-Contacted Google Cloud Support and G Suite Support to make sure settings are correct (they don't support script or contacts.)
Problem 2:
I've tried to add the Oauth 2 code but now it gives me the error "You do not have permission to call getActiveSpreadsheet."
Are their problems in my Oauth 2 code below? (I removed my personal info.)
function getContactsService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('contacts')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('blablabla.apps.googleusercontent.com')
.setClientSecret('blablabla')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.google.com/m8/feeds')
.setScope('https://www.googleapis.com/auth/spreadsheets')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
.setParam('login_hint', Session.getActiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Forces the approval prompt every time. This is useful for testing,
// but not desirable in a production application.
.setParam('approval_prompt', 'force');
}

Are you logged into multiple Google accounts? That often causes issues with authorization. Try logging out of everything and logging back in to just one account.
Is this running on a trigger or are you manually running the formulas?
Are the owner of the spreadsheet and logged in as the owner? Are you running this test in the spreadsheet's container-bound script?
If you're the owner of the spreadsheet and you're just adding the email addresses from your own spreadsheet to your own Gmail account then there's no reason you need to use the API or OAuth protocol.
As another commenter mentioned, what are the scopes you're requesting? To just set up a simple script from your spreadsheet to add contacts you probably just need the two you listed. You can set them manually by selecting view-->show manifest file. For example:
{
"timeZone": "America/New_York",
"dependencies": {
},
"oauthScopes": [
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/spreadsheets"
]
}

Related

OAuth 2.0 from Google Apps Script (GAS) to Sheets API

Based on the Google's documentation, I've implemented the following code to append a row to the Google Sheet (and I'm owner of this sheet).
function addSalaryLog(updateArray) {
const resource = {
"majorDimension": "ROWS",
"values": [updateArray]
};
const spreadsheetId = 'here_goes_the_spreadsheet_id';
const range = 'A:A';
const optionalArgs = {valueInputOption: "USER_ENTERED"};
Sheets.Spreadsheets.Values.append(resource, spreadsheetId, range, optionalArgs);
}
When I call it, in return I get the following error:
GoogleJsonResponseException: API call to
sheets.spreadsheets.values.append failed with error: Request is
missing required authentication credential. Expected OAuth 2 access
token, login cookie or other valid authentication credential. See
https://developers.google.com/identity/sign-in/web/devconsole-project.
No need to ask, I clicked the link :-) It explains how to create an OAuth 2.0 in the developer console. Created ✅. But later it does not explain how to actually authorize the script.
I expect that before calling the Sheets API, I should be able to trigger launching the permission dialog.
I see the following, possible options:
Instead of Sheets.... use URLFetch.
Trigger the authentication (preferred).
Any tips here?
UPDATE
I've implemented testAddSalaryLog() which is exactly the same as the original function but has hardcoded values. And it works! It makes me think that the solution is to force the permission screen to trigger.
I have 3 questions/suggestions:
Question 1.
Since you are using the Sheets API. Have you enabled the Google Sheets API advanced service in your script? From Editor > Service > Google Sheet API?
Question 2.
Have you reviewed if the "Sheets API scope" were added to the script by reviewing the project Overview? If they were not added automatically, you need to add them manually by following these steps:
Access the 'Project Settings' to enable the manifest file in the editor.
Return to the editor and select 'appsscript.json'
Add the scopes:
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/spreadsheets"
Question 3.
Have you authorized the code already?
In the Apps Script editor, click Run. The first time you run the sample, it will prompt you to authorize access:
Select Review permissions > Allow.
Reference:
Method: spreadsheets.values.append scopes
Google Apps Script Quickstart for Sheets API
Advanced Sheets Service
Setting explicit scopes for Apps script

Oauth2 service creation hasAccess() fails in Google Sheets script for all users except owner

I'm using the https://github.com/googleworkspace/apps-script-oauth2 library in a Google Sheet script to call an external API. It works well for the owner of the sheet. For all other users it fails in creating the service/property store? It fails the "Service.hasAccess()" condition.
I suspect I am missing some sort of permissions somewhere. I have given users Edit permissions on the Sheet and have gone through other various gyrations trying to figure this out. I decided to apply this script via a Standard Project.
Scope are applied explicitly in the manifest and all works swimmingly for the sheet owner.
''''''''Google Apps Script, Spreadsheet Script in GCP Standard Project
function authorizeUser() {
var myService = getMyService();
if (myService.hasAccess()) {
>FAILS THIS CONDITION for all except spreadsheet owner
}
}
function getMyService() {
return OAuth2.createService('sky')
.setAuthorizationBaseUrl('https://oauth2../authorization')
.setTokenUrl('https://oauth2.../token')
.setClientId('fee......')
.setClientSecret('Ighh.....')
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
//.setScope('https://www.googleapis.com/auth/drive')
// Below are Google-specific OAuth2 parameters.
.setParam('login_hint', Session.getEffectiveUser().getEmail())
}
>I believe the failure is occurring in OAuth2.gs here
function createService(serviceName) {
return new Service_(serviceName);
}
OAuth2.gs: https://github.com/googleworkspace/apps-script-oauth2/tree/master/dist
Any thoughts?
D M
Apparently the suggested code to validate the service state in the apps-script-oauth2 library is not indicative of whether or not the Oauth process can be completed.
Direct the user to the authorization URL
Apps Script UI's are not allowed to redirect the user's window to a new URL, so you'll >need to present the authorization URL as a link for the user to click. The URL is >generated by the service, using the function getAuthorizationUrl().
function showSidebar() {
var driveService = getDriveService();
if (!driveService.hasAccess()) {
I was able to complete my Oauth process regardless of the state returned by has.Access() . I'm not sure if this is a time sensitive operation or something else is at play. At any rate I was able to proceed and develop a final solution as a GAS web app.

Securely calling a Google Cloud Function via a Google Apps Script

How can I securely call a Google Cloud Function via a Google Apps Script?
✅ I have a Google Cloud Function, which I can access at https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION, and which I would like to allow certain users to invoke via an Apps Script.
✅ To secure the Cloud Function, I have set Cloud Function Invoker to only include known email (e.g. USER#COMPANY.com, where this is a valid Google email).
✅ I am able to successfully invoke the Cloud Function via curl, while logged into gcloud with this email, by running: curl https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION -H "Authorization: Bearer $(gcloud auth print-identity-token)".
✅ I have granted the following oauthScopes in my Apps Script's manifest:
"https://www.googleapis.com/auth/script.external_request"
"https://www.googleapis.com/auth/userinfo.email"
"https://www.googleapis.com/auth/cloud-platform"
⛔️ However, when I attempt to invoke the Cloud Function via a Google Apps Script, while logged in with the email USER#COMPANY.com, I am unable to invoke it and instead returned a 401. Here is how I have attempted to invoke the Cloud Function:
const token = ScriptApp.getIdentityToken();
const options = {
headers: {'Authorization': 'Bearer ' + token}
}
UrlFetchApp.fetch("https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION", options);
ℹ️ I have also tried the following:
Using ScriptApp.getOAuthToken()
Adding additional oauthScopes, e.g. openid.
Creating an OAuth Client ID with https://script.google.com set as an Authorized Javascript origin.
Deploying the Apps Script.
Crying out to the sky in utter, abject despair
I struggled very much authenticating from Apps Script to invoke a Cloud Run application and just figured it out, and I believe it's similar for calling any Google Cloud application including Cloud Functions. Essentially the goal is to invoke an HTTP method protected by Google Cloud IAM using the authentication information you already have running Apps Script as the user.
The missing step I believe is that the technique you're using will only work if the Apps Script script and Google Cloud Function (or Run container in my case) are in the same GCP project. (See how to associate the script with the GCP project.)
Setting it up this way is much simpler than otherwise: when you associate the script with a GCP project, this automatically creates an OAuth Client ID configuration to the project, and Apps Script's getIdentityToken function returns an identity token that is only valid for that client ID (it's coded into the aud field field of the token). If you wanted an identity token that works for another project, you'd need to get one another way.
If you are able to put the script and GCP function or app in the same GCP project, you'll also have to do these things, many of which you already did:
Successfully test authentication of your cloud function via curl https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION -H "Authorization: Bearer $(gcloud auth print-identity-token)" (as instructed here). If this fails then you have a different problem than is asked in this Stack Overflow question, so I'm omitting troubleshooting steps for this.
Ensure you are actually who the script is running as. You cannot get an identity token from custom function in a spreadsheet as they run anonymously. In other cases, the Apps Script code may be running as someone else, such as certain triggers.
Redeploy the Cloud Function as mentioned here (or similarly redeploy the Cloud Run container as mentioned here) so the app will pick up any new Client ID configuration. This is required after any new Client ID is created, including the one created automatically by adding or re-adding the script to the GCP project. (If you move the script to another GCP project and then move it back again, it seems to create another Client ID rather than reuse the old one and the old one will stop working.)
Add the "openid" scope (and all other needed scopes, such as https://www.googleapis.com/auth/script.external_request) explicitly in the manifest. getIdentityToken() will return null without the openid scope which can cause this error. Note to readers: read this bullet point carefully - the scope name is literally just "openid" - it's not a URL like the other scopes.
"oauthScopes": ["openid", "https://...", ...]
Use getIdentityToken() and do NOT use getOAuthToken(). According to what I've read, getOAuthToken() returns an access token rather than an identity token. Access tokens do not prove your identity; rather they just give prove authorization to access some resources.
If you are not able to add the script to the same project as the GCP application, I don't know what to do as I've never successfully tried it. Generally you're tasked with obtaining an OAuth identity token tied to one of your GCP client ids. I don't think one app (or GCP project) is supposed to be able to obtain an identity token for a different OAuth app (different GCP project). Anyway, it may still be possible. Google discusses OAuth authentication at a high level in their OpenID Connect docs. Perhaps an HTML service to do a regular Google sign-in flow with a web client, would work for user-present operations if you get the user to click the redirect link as Apps Script doesn't allow browser redirects. If you just need to protect your service from the public, perhaps you could try other authentication options that involve service accounts. (I haven't tried this either.) If the service just needs to know who the user is, perhaps you could parse the identity token and send the identifier of the user as part of the request. If the service needs to access their Google resources, then maybe you could have the user sign in to that app separately and use OAuth generally for long term access to their resources, using it as needed when called by Apps Script.
The answer above is very good. But since I am new with this I still had to spend a lot of time trying to figure it out.
This worked for me:
Apps Script code:
async function callCloudFunction() {
const token = ScriptApp.getIdentityToken();
const options = {
headers: {'Authorization': 'Bearer ' + token}
}
const data = JSON.parse(await UrlFetchApp.fetch("https://MY_REGION-MY_PROJECT.cloudfunctions.net/MY_FUNCTION", options).getContentText())
return data
}
Make sure that in project config you have the same project where your function is created.
After that, you can add the emails of the users you want to access the script on the Permission section in the function.
And as #alexander-taylor mentioned as well, make sure to add the scopes to your manifest file. You can make the manifest visible from the configuration tab in apps script. It took me some time to get that too.
Thanks to your comment you can do 2 things. But before, you have to know that you can't (or I least I never achieve this), create a valid identity token for being authenticated by Cloud Function and Cloud Run with a user credential. I opened a question on this
But you can call Google Cloud API with user credential! So
You can use the function test call API. The quotas limit you to 16 calls per 100 minutes (of course, it's design for test!)
You can publish a message into PubSub and plug your function on it. In this pattern your call is asynchronous.

OAuth not granting access while using Google Apps Script

I'm trying to call StackExchange's API, using Google Apps Script and Google Sheets. I can't figure out where in the OAuth process things are going wrong. My current code is not granting access:
function getStackExchangeService_() {
var CLIENT_ID = PropertiesService.getScriptProperties().getProperty('SE_CLIENT_ID');
var CLIENT_SECRET = PropertiesService.getScriptProperties().getProperty('SE_CLIENT_SECRET');
return OAuth2.createService('StackExchange')
.setAuthorizationBaseUrl('https://stackoverflow.com/oauth')
.setTokenUrl('https://stackoverflow.com/oauth/access_token')
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setRedirectUri('https://stackexchange.com/oauth/login_success')
.setScope('global');
}
When I call this and log the response I always get "false":
var service = getStackExchangeService_();
Logger.log(service.hasAccess());
Thanks for the help!
The most obvious issue is that global is not a valid Stack Exchange scope.
Probably use .setScope('read_inbox'); to start.
Also, be sure that:
You have registered your app and configured it for explicit (server-side) OAuth2. (Omit the port information in your case.)
Apparently you should use https://script.google.com/macros/d/{SCRIPT ID}/usercallback for the Application Website, per these instructions??.
Which means you would use script.google.com for the OAuth Domain.

Modify the signature for all users in my domain

Currently my code only modifies my signature, because when I put the email of the other person in my domain, the error: Not Found (line 9, file "Code") appears.
My current code:
function myFunction() {
var newSignature = Gmail.newSendAs();
newSignature.signature = "signature";
var listEmails = [
"leticia#domain.com"]
var updateSignature = Gmail.Users.Settings.SendAs.update(newSignature, "me", listEmails)
}
I am developing using APPS SCRIPT.
Any suggestions for me to be able to change the signature of someone else in my domain?
To change other people Gmail settings in your domain you'll need to be a domain Admin, then create a service account with domain-wide authority, then whitelist it in the Admin Console. Then use said service account and authentication token generated to authenticate your requests to the Gmail API.
This built-in Apps Script Gmail integration was not made for that use-case. The intended usage is to setup your own settings, or individual users that explicitly authorize your application to run on their behalf. This sendAs is there because one might have multiple Gmail signatures, depending on their selected send-as/from alias.
Note that simply authorizing an script with your GSuite admin account won't allow to the script to perform domain-wide operations. That'd be too dangerous, therefore the somewhat convoluted service-account setup is required.
Link to the relevant documentation