Over the last few months I have become familiar with OAuth2.0 authorization within google apps script but a recent anomaly has me confuzzled. I have a standalone web-app that acts as a front-end to a fusion table. The web-app is set to 'execute as' the script and fusion table owner and the fusion table grants view access to external users. The program detects authorization, prompts if needed, and uses refresh tokens if they're available. When run from the account that owns the script and fusion table, all is well.
Once I published the web-app, I tested it from an external account and it worked fine. The refresh token was then removed from the script-owner's UserProperties. When the app was run again from an external account, it prompted for authorization and authorized correctly (saving the new tokens in the UserProperties of the script-owner). However, the next POST call to the API received a 403-Forbidden error. At this point the app will continue to receive 403-Forbidden errors from any account (including the script-owner's account) until the tokens are manually cleared and reauthorized as the script-owner.
Why is it that these tokens are not valid? I would expect that since the app is being 'executed as' the script/fusion owner, any tokens received programmatically would be as valid as those those authorized by the script/fusion owner. If my expectations are incorrect, how can I protect against this situation for multiple users?
Update
I've gained some more traction on this and have identified some related issues that I wanted to share here. First of all, I was manually deleting the tokens (refresh and access) to test the ability of the app to authorize. Subsequent authorizations were not returning a refresh token (which caused excessive prompting). I found out that this is an intentional outcome from the google API. Aside from the requisite request parameters to return a refresh token, I found out that a refresh token is only returned on the first user-prompt and authorization(ref. here). In order to get another refresh token, you need to either revoke-acess and re-authorize or force the approval prompt in the request. Once I fixed that it became much clearer that the token was 'attached' to the user making the initial authorization request. As long as I retained the refresh token acquired by the script/table owner, then the script could be used (and reauthorized programmatically using the refresh token) by any external user. Which brings me back to the point. If I lose the refresh token, I need to manually delete all remaining token scraps, login as script/table owner, revoke access to the app, then reauthorize.
Per Zig's answer below, that is just how it is. Is there no way I can programmatically prevent that very manual process?
Script executing as owner will save owner tokens automatically through appscript builtin authentication.
However if you are doing the oauth flow yourself, it will save the user's token and thus wont have access to your data.
Related
In a GAS published as webapp bound to a spreadsheet in a public (free) Gmail account, I'm compiling information from many unrelated users into that spreadsheet, where any Gmail user account can access the webapp.
But when script is run as the user, the user can access the app (after user grants permissions to the app), but it stops at point where script accesses the spreadsheet. The user is told to request access. I have to "share" the spreadsheet to the user for the app to function when run as that user, but that is problematic--too many potential users.
If another version of the script is run instead under my account, then user has access to the webapp, but the script cannot access the user's Gmail address. Big problem because the webapp must have the user's Gmail address for security/application access controls.
(The webapp is in development and Google "approval" has not yet been sought.)
How do I have a webapp (a) that modifies content in a central spreadsheet, (b) that any Gmail user can access, (c) where the script has access to Session.getActiveUser().getEmail(), and (d) the user does not need to have edit permissions for that central spreadsheet? The only user-account information that the script needs is the user Gmail address--again for application access control and security. (Since Google provides me a link that gives anyone full access to the spreadsheet (if they also have its URL), can't I give the bound script access to the spreadsheet when run under any Gmail account, where the integrity of the spreadsheet content is managed through the app's functions and internal access controls?)
If there is no solution except to run the webapp under the user's Gmail account and give that user edit rights to the spreadsheet, would that create significant risk (where I don't intentionally/overtly disclose the spreadsheet's URL) that a user could access and alter the spreadsheet beyond the insert/edit functions performed by the webapp? If that risk is low, is there a function I could use in the webapp to automatically grant spreadsheet edit rights to a Gmail account when its Gmail address is pre-registered in the user list for the webapp?
Thank you!
Let me start with c): there is no way to make that Session.getActiveUser().getEmail() works for free Google accounts (i.e. gmail.com accounts) other than for the container / script owner.
Regarding a) b) and d) you might use the Google Sheets API making the calls to it by using UrlFetch Service and a service account instead of the Spreadsheet Service or the Advanced Sheets Service.
Regarding allowing anyone to edit a central spreadsheet, once they open it either by using the Google Sheets apps or by means a web app, they will be able to find the spreadsheet in Google Drive > Shared with me among other places... IMHO the risk in terms of how likely is to have issues is big but in terms of the the impact depends on several factors, i.e. some changes might be reverted by using the version history but spreadsheet might become corrupted. If you have not used Google Sheets version history intensively the best is to get deeply familiar with it before relying on it as a backup / disaster recovery main tool .
From https://developers.google.com/apps-script/reference/base/session#getactiveuser
getActiveUser()
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user). However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same Google Workspace domain as the user.
Related
Use Apps Script URLFetchApp to access Google Datastore Data
Google Service Accounts / API - I keep getting the Error: Access not granted or expired. (line 454, file "Service")
When I call Session.getActiveUser() from a function called by an installable trigger (e.g. an OnOpen Event trigger in Google Sheets - but not the Simple Trigger) in Apps Script, what user information am I getting?
I understand that installable triggers run under the owner/creator of the trigger regardless of who has opened the sheet. So would this always return that person's info?
Either way, how do I get the information of the other person? (e.g. if it gives owner info, how do I get the info of the user actually opening it - and vice-versa)
Update:
I got another user to test my script. I watched the logs while they were in the file, and it definitely reported THEM as the user, even when the installable OnOpen trigger was triggered.
This is good from the perspective that it showed them the correct menu options - he and I saw different menus per my OnOpen script, which is what I want.
However, this raises two issues for me:
This seems to go against the Google Documentation, which states: "Installable triggers always run under the account of the person who created them. For example, if you create an installable open trigger, it runs when your colleague opens the document (if your colleague has edit access), but it runs as your account. This means that if you create a trigger to send an email when a document is opened, the email is always sent from your account, not necessarily the account that opened the document."
In a future function, I will be calling an API from another App. This API will need my credentials (API ID and Secret). I was hoping / expecting that I could "sandbox" my credentials in an installable trigger - invisible to other users - that will allow them to use my credentials just for the specific functions which I would script into the API. If the installable trigger is in fact, NOT using my credentials, then how can I do this? I don't want to have to make every user go to the other App and generate their own set of API credentials, that will be unsustainable in this organization, and not everyone should need to do that.
It should return whomever triggered the script. But it depends if the security policy does allow you to access the user's identity. Seeing the documentation:
Gets information about the current user. If security policies do not allow access to the user's identity, User.getEmail() returns a blank string. The circumstances in which the email address is available vary: for example, the user's email address is not available in any context that allows a script to run without that user's authorization, like a simple onOpen(e) or onEdit(e) trigger, a custom function in Google Sheets, or a web app deployed to "execute as me" (that is, authorized by the developer instead of the user).
I have tested it and even an installable trigger won't return anything if it belongs to a different organization.
But you might be able to if other users belong to the same organization.
However, these restrictions generally do not apply if the developer runs the script themselves or belongs to the same Google Workspace domain as the user.
Workaround:
One thing I guess would be to assign the triggered function into a button and have the users click that upon opening the sheet. Via clicking the button, I have been able to show the User object using that method.
Or a webapp that will serve as a relay and will get the User details.
Getting a “Google hasn’t verified this app” warning when creating a trigger to run an app script on form submit (full warning text included below). It says until the developer verifies this app with Google, you shouldn’t use it.
This is purely an internal script and internal app being used in only our very small company. Does it still need to be “verified” by Google? It gives pretty dire warnings about what the app will be allowed to do, even though it is a simple app that doesn’t perform all those privileged things.
Overview of the app … a Google Form that stores responses in a Google Sheet. An app script (residing in the sheet) basically makes a copy of a Google Docs template file, and populates the copy with data from the sheet. We have a trigger to run the script on form submit. The warning is presented when the trigger is created, even when the same person is the owner of all parts – the app script “project”, trigger, form, sheet, docs, and shared folder on Google drive.
The warning just seems to pop-up once. In this scenario is it fine to just ignore it?
Full warning text below …
Google hasn’t verified this app.
The app is requesting access to sensitive info in your Google account. Until the developer (script owner’s email) verifies this app with Google, you shouldn’t use it.
Continue only if you understand the risks and trust the developer (script owner’s email).
Link: Go to (app_script_name) (unsafe)
… after clicking the link you get …
(app_script_name) wants to access your Google account
This will allow (app_script_name) to:
• See, edit, create and delete all of your Google Drive files
• See, create, and edit all Google Docs documents that you have access to
• See, edit, create and delete your spreadsheets in Google Drive
• View and manage your forms in Google Drive
Make sure you can trust (app_script_name)
You may be sharing sensitive info with this site or app. Learn about how (app_script_name) will handle your data by reviewing its terms of service and privacy policies. You can always see or remove access in your Google account.
(Buttons for Cancel or Allow)
If you wrote the script yourself, or trust its developer, there is no reason to not authorize the app.
Your consent screen is being shown, but your app has not been reviewed
If your users are seeing the "unverified app" screen , it is because you haven't submitted your app for verification or your OAuth request includes additional scopes that haven't been approved.
This warning shows up when:
Your app uses sensitive or restricted scopes and you haven't configured them in your OAuth consent screen configuration page and requested verification.
Your app uses sensitive or restricted scopes that you haven't selected on the OAuth consent screen configuration page.
You selected sensitive or restricted scopes on the OAuth consent screen configuration page and requested verification, but the verification is not yet complete.
References
Unverified apps
Verification Status
Does anyone know how to obtain a refresh token for a G Suite Add on, considering that the authorization flow to obtain the user consent was handled by Apps Script?
I know how to obtain the access token with the method
ScriptApp.getOAuthToken()
But I don't know how to obtain the refresh token for a particular user.
Context:
I have an add-on that allows users to schedule recurring meetings. The user grants the app access to their calendar when they install the add-on from the G Suite Marketplace, so I can read and write on their calendars when they are running the script.
I want to send them email reminders a day before a meeting, so I want to read their calendars in the background using a cloud function when they are not running the script.
In order to do that I would need to have the refresh token for that user since the access token expires.
I'm really stuck on this for many days now. Any help would be much appreciated!
Thanks a lot.
You have two options:
Either you get the refresh token with Url fetch as shown in the Generate an OAuth2 refresh token documentation
Or you install the apps-script-oauth2 library
If you set access to offline (.setParam('access_type', 'offline')) it is enough if the user grants you permission once.
We are looking for a way to trigger a function in the add-on in the user's context (authorization) from our web app. We tried using Apps Script Execution API to trigger that. The flow is add-on (apps script) registers the access token in our web app and the web app triggers execution API using the access token. This works for the first time. But the problem is, the access token is valid for only a few minutes. After which the web app is not able to initiate function through execution API due to authorization failure.
Questions:
Is our approach for triggering add-on/ apps script function from another web app in the user's context correct by using execution API?
If so, how do we avoid access token from expiring? Apparently, there is no API in apps script to 'get the refresh token'/ 'refresh the token'. How do we go about refreshing the token so that the token is valid forever (until the user cancels)?
Is there any radically different approach that will help in this case?
I see that add-ons like "PearDeck", "Form Approvals", "Form Publisher" etc are successfully doing this (triggering add-on function from the web app with user's authorization).
Hope to get some expert advice in this forum. Thanks
Used Execution API - with the problem in token getting expired
function prepareToken() {
// send to web app
var token = ScriptApp.getOAuthToken();
URLFetchApp.fetch("url?token="+token);
}
function doSomeWorkFromUserContext() {
// impl here
}
POST https://script.googleapis.com/v1/scripts/{scriptId}:run
Request Header
Authorization: Bearer + {token}
Request Body
{
"function": "doSomeWorkFromUserContext"
}
The function doSomeWorkFromUserContext should get called all the time of invocation but getting Authorization error after some time.
When a user authorizes your app for the first time you receive a refresh token along with the access token. The access token typically expires after one hour and you have to use the refresh token to fetch a new access token.
Note that the refresh token is only returned when the user initially authorizes your app.
You'll need to revoke the user's authorization (see below) and have them reauthorize the app to get the refresh token. Revoking access can be achieved in one of 3 ways:
Have the user uninstall the app
Use the ScriptApp.invalidateAuth() function
Make a HTTP GET request to the OAuth2 revoke endpoint with the users access token ("https://accounts.google.com/o/oauth2/revoke?token={token}")
Eric Koleda's GAS OAuth2 library does a great job of managing the entire OAuth2 flow (including automatically refreshing access tokens) so be sure to check that out.