Limiting Google Apps Script Web app to certain users only - google-apps-script

Making a web app that makes changes to certain (Sheets) files on my Google Drive (i.e. user will use the app as me), but I would like to restrict the Web app access
only to certain users. When deploying app, I only have the options of making it private or public.
One solution would be to use the session class to see if a correct user is logged in.
function onAppBegin(){
if (Session.getActiveUser().getEmail() != "correctappuser#gmail.com") return null;
accessGranted();
}
However, I am concerned if this crude method is actually safe and is not hackable?

The method is too safe: nobody will have access. If your web app is deployed with the option "Execute the app as: me", then Session.getActiveUser().getEmail() will probably return the empty string. See documentation:
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 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 and the user belong to the same G Suite domain.
The issue is that even though the user logged in to access the web app, they did not authorize it to do anything on their behalf, e.g., find their email address.
If the web app is deployed to be executed by "User accessing the web app", then they will be asked to authorize it, and so the web app can learn their identity. But then, it will only be able to modify those files that the user already can modify directly.
The way I get around this difficulty is by giving the authorized users a key to the web app (some long random string). They access the app by going to https://..../exec?key=mykey, and the app checks the key as follows:
function doGet(e) {
if (e.parameter.key == "mykey") {
var ss = SpreadsheetApp.openById("spreadsheet Id");
// modify the spreadsheet
}
}
Now, this is even more crude than your original approach, but it works. If a wrong person gets the key, they will be able to use the app but the damage will be limited to what the app can do. It's not nearly as bad as someone getting access to your Google Account.

Combining the two ideas works great.
Checking to make sure it's the right user, and requiring a key.
If not both are correct it displays a no access page, instead of the real page.
function doGet(e) {
if (Session.getActiveUser().getEmail() != "good#email.here") {
var t = HtmlService.createTemplateFromFile("pageNoKey");
return t.evaluate().setTitle("userDelegation No Access");
} else if (e.parameter.key == "SomeKeyHere") {
var t = HtmlService.createTemplateFromFile("page");
return t.evaluate().setTitle("PageTitle");
}
var t = HtmlService.createTemplateFromFile("pageNoKey");
return t.evaluate().setTitle("PageTitle No Access");
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
My code can probably be improved. I'm still learning. (Maybe validate both values in the first if loop?)
Anyway, this is what I'm using to restrict access to users managing Gmail delegation, using my free tool userDelegation. Since it's powered by a service account with domain wide rights, it needs to be locked down hard.
If you want a really flexible solution, check out this video, where the validation is done based on what type of access users have to a specific Google Doc! That was unnecessarily complex for my situation, as I only needed to restrict access to a single person accessing my web app. I will instead create separate versions of the same web app, and give each admin their own.
If you need an easy way to restrict your web app to several people, then that method might be just right for you.

Related

How to use OAuth 2.0 with a Google Apps Script library, with a static redirect URL?

I can't figure out how to make a shared Google Apps Script library, that uses OAuth 2.0.
The problem is that the usercallback redirect URL changes, every time I use the library in a different script. However, that means I'd need to add a new app and whitelisted redirect URL to Asana for each spreadsheet I use the script in. I'm using https://github.com/googlesamples/apps-script-oauth2.
Is there a way to always authenticate with the same redirect URL, so that the library I make can be used from any script, without registering a new redirect URL in Asana?
I'm a Developer Advocate here at Asana. If I understand your question correctly, then yes, you'll have to handle the callback separately for each script. For security reasons, we validate that the OAuth app registration registers the same url as an integration actually requests when authenticating. If this weren't true, for instance, it'd be possible to create a malicious script that uses the client_id from a legitimate script but asks for the redirect to go to its own credential-grabbing endpoint. This is fixed if the app that got the client_id on app registration also specifies precisely which endpoint should be the legal endpoint to redirect to. That means each OAuth app needs to have its own unique and consistent redirect URL :(
I suppose you could possibly create a single "router" Google Apps script which would set the state parameter with some user/script pair when hitting Asana's oauth_authorize endpoint and forward the user credentials on to the script that exists behind the router script based on that user/script pair when the response comes back, but it's not super trivial.
One final option would be to use a Personal Access Token to access Asana's API. This one token can be used by an unlimited number of scripts for access. The downside is that this token "looks like you", that is, it takes action on behalf of not a third party user but you yourself - your scripts would be an automated version of the user whose Personal Access Token they use. This can be mitigated to some extent by creating a "bot account" to access our API and giving it access inside of Asana to the projects or teams you want to gather data on. The other downside to this approach is that every script that uses the personal access token will break if you ever revoke the one token, so if that ever happens by either intent or accident, you'll have to update the Personal Access Token information in every script that uses it.
Hopefully this helps you to evaluate the options and choose which one of these options works best for your script.

In Google Apps Script, avoid second 'Review Permissions' prompt, possibly by using approval_prompt

I have searched StackOverflow, etc. for a solution to this problem, and several answers 'point me in a direction' (mentioning approval_prompt = auto not approval_prompt = force), but none are applicable (as far as I can tell) to my situation.
I have a Web Application hosted at www.mjpanel.com that expects to use a Google Apps Script that I 'own', but the Javascript at www.mjpanel.com calls the Google Apps Script (deployed as a Web App with doGet()) as a Web Service. It expects the web service call to return various JSON objects.
If the user has not yet authorized my application, the call to the Google Apps Script Web App / Web Service will not return a JSON object, causing www.mjpanel.com Javascript code to fail saying "Invalid Request" (because it isn't a JSON object as my code expects).
To prevent this from happening, www.mjpanel.com uses gapi.auth2.init to get the permissions/scopes it needs. I'm developing everything now, so if/whenever the Google Apps Script evolve to use something (like sending GMail emails as the user) that is new, I have been figuring out the scope to request, adding it to the list of scopes in the gapi.auth2.init call, and everything is fine. The next time a user uses the app., they get initially prompted for the newly added scope, then everything proceeds fine.
However, now sometimes one of my test users has a Web Service call fail because Google Apps Script is returning another request for permissions for a 'new permission' of 'Have Offline Access'.
There's nothing about my script that would warrant the user needing to grant this permission.
When I research, a lot of stuff (mostly about requesting OAuth2 stuff in a 'structure' different than the way my app. is set up) says it has to do with submitting a 'approval_prompt=force' in my request URL.
However, the way I have my app set up, all the URLs I would use (aside from my 'custom stuff' in the query string) are dictated by Google Apps Script. And I can't find any place where any URL I use has an approval_prompt in it.
I can't figure out where I would need to configure that approval_prompt to be auto (as is recommended in the 'successful answers' I find).
Of if the idea of approval_prompt is 'on the wrong track', any information in general to help me solve this problem would be greatly appreciated.
Thanks in advance for any help you can give me.
Unfortunately the Google Apps Script native OAuth flow includes approval_prompt=force. This causes the following conditions:
If cookie exists in browser and has permission for this application the consent screen will not be displayed.
If cookie exists in browser but does not have permission for this application: consent screen will be displayed
If cookie does not exist in browser and the application has permissions: application will request 'Have offline access'.
The easiest way around this is to manage your own OAuth flow and use the Execution API. The following link will take you to the javascript quick start.
https://developers.google.com/apps-script/guides/rest/quickstart/target-script

How to connect to the Facebook Graph API from Google Apps Script?

I'm trying to connect to the Facebook Graph API via a Google Apps Script but I'm getting an error
I've tried:
function myFunction (name) {
FB.init({
appId : '{your-app-id}',
status : true,
xfbml : true,
version : 'v2.0'
});
var jsonData = UrlFetchApp.fetch("graph.facebook.com/"; + name);
}
I've also tried:
function myFuntion(name) {
window.fbAsyncInit = function() {
FB.init({
appId : 'your-app-id',
xfbml : true,
version : 'v2.0'
});
};
var jsonData = UrlFetchApp.fetch("graph.facebook.com/"; + name);
}
but neither have worked, I always get a:
"ReferenceError: "FB" is not defined." and a "ReferenceError: "window" is not
defined"
and
"(#4) Application request limit reached","type":"OAuthException","code":4}}
despite putting in my facebook app ID into the variable. I know that "window" is part of an external javascript library so that's why I'm unable to use it in a Google Apps Script, but even after looking at other posts I'm still confused on why I get a "FB" is not defined error.
Any ideas on how to solve this?
There are error codes at the bottom of this page:
Facebook Graph API - Error codes
The "OAuthException" has to do with the Login Status. If you get that error, then you aren't logged in, and to get what you want, you need to be logged in.
You can get an App Access Token using a Server to Server request. There are four types of
Access Tokens:
User - to read, modify or write a specific person's Facebook data on their behalf.
App - modify and read the app settings, and publish Open Graph actions.
Page - read, write or modify the data belonging to a Facebook Page.
Client - the client token is used rarely. Very limited Access to Facebook.
Forms of Access Tokens
User access tokens come in two forms: short-lived tokens and long-lived tokens
short-lived - lifetime of about an hour or two - generated via web login
long-lived - lifetime of about 60 days
You probably don't have an App Access Token. You have an App ID, but that's different than an App Token.
You only get your App Token once. You need to run some code to get it.
Note, that you also must know your App Secret in order to run this code. If you don't know, or have your App Secret, then you need to get that.
See if you can run this code:
//A Facebook App Token never changes unless you go to the Facebook Developers Console,
//and you
//change the App Secret. So, do NOT keep requesting a new App Token. Just get it once,
//then
//hard code it into a backend secret function.
// The App Token can be used to modify your App, but you can just do that 'Manually'
function getOneTimeFB_AppToken() {
Logger.log("getOneTimeFB_AppToken ran");
//keep this info secret
//Generate an App Access Token
var myApp_ID = 'Your App ID';
var myAppSecret = 'Your App Secret';
var optnAppTkn = {"method" : "get"};
var getAppTknURL = "https://graph.facebook.com/oauth/access_token?client_id=" + myApp_ID + "&client_secret=" + myAppSecret + "&grant_type=client_credentials"
var getAppTkn = UrlFetchApp.fetch(getAppTknURL, optnAppTkn);
Logger.log("Object returned from GET: " + getAppTkn)
var myAppTkn = getAppTkn.getContentText();
Logger.log("myAppTkn: " + myAppTkn);
};
Run that code, then in the script editor, choose the VIEW menu, and the LOGS menu item. Read what is in the LOGS. Don't keep running this code over and over again. Just run it once if it's successful.
If that code works, then you just successfully communicated with Facebook.
You need to understand what the Tokens can do, and what your options are. If you are not going to get a token from a user through client side authorization, then you need to understand the App Token.
App Tokens allow you to interact with Facebook on behalf of an app rather than a user. This can be used to read YOUR app insights and modify the parameters of YOUR app.
You never want to use an App Token in client side (browser) code. That would be a major security problem.
However, if a user has granted your application publishing permissions, then you can use the App Token to publish content to Facebook on behalf of that person. So, app access token can be used in place of a user access token to make API calls IF your app has been granted publishing permissions.
But how do you get publishing permissions? Well, there is no way to get the initial short term access token through the server. That just makes sense if you think about it in terms of security. You can't get the initial, short term access token any other way than through a client login. So, if you want to do something that isn't within the bounds of the App Access Token, you can't do it without having the user login through client side.
You can achieve a client side login, without using the JavaScript SDK. So, in the case of an Apps Script Stand Alone HTML web app, you can still use Facebook login without needing to link to the Facebook JavaScript SDK. If you need to do that, let me know.
In that code, FB is an object. The object needs to be assigned "key/value" pairs. Every "key/value" pair is an element (property) in the object. The error is directly related to how objects work. That FB object gets assigned values from a link (inside HTML) to the Facebook API. If you are trying to use an HTML link to the Facebook API from server side (.gs) code, it won't work. There are lots of things that could be going wrong. In order to know exactly what is going wrong, we need to know whether that code is in a gs file, or an HTML file inside a <script> tag.
There are a couple of ways to connect to Facebook:
From HTML (Client Side)
From the server with HTTP Requests
It looks like the code you are using is from an example of how to use the Facebook JavaScript SDK that is meant to run from inside HTML. The problem with that, is that Apps Script sanitizes HTML sent to the browser. So, if you try to link to the Facebook JavaScript SDK through the HTML, you may not get access. I know that, in the past, I have not been able to use a link to the Facebook API in HTML with the NATIVE sandboxed mode. I haven't tried the new IFRAME sandbox mode.

Google Script OAuth for multiple users

I've created a Google App Script that handle 2 different OAuth connections.
1- Google itself to send mail on behalf of the user and access google docs (google api console used to get keys, secret)
2- gtraxapp wich is a timesheet cloud-based app. (Script is registered, got a key/secret, etc.)
The script is published as a web app. It works perfectly for my user.
When logged on a different user name, I can authorize Google OAuth without providing different key/secret, and emails will be sent from the actual user.
Problem happens with the 2nd app (gTrax).
Authorization seems to work. Running the function inside the script to authorize lead to a screen asking for permission, gtrax then appears in the account as a registered app (could revoke access if needed).
But, when running the app, I get a message saying I need permission to do this action (UrlFetchApp / simple get)
My question is :
Is this possible that I need to register each user to get a key/secret for everyone (and dealing with that in the script)...
Or do OAuth can be registered with 1 key/secret ?
In other word, are (should) key/secret linked to a single user or are they only a kind of RSA-like key pairs that, when verified, can be used to authorize any user.
My understanding is this. When you use built-in Apps Script functions, like MailApp.sendEmail, the Google Apps Script "environment" takes care for you to ask authorization for the user (1st time he access your app) and save and manage the oAuth tokens for you, so it all runs smoothly.
When you call an external service using UrlFetchApp, Apps Script oAuth authorization process works differently. The authorization is just a strange popup you get on the script editor, when you actually make the fetch call. It is not processed at "compile time" and asked before you run anything like the other services. But you also do this step only once.
The "gotcha" is that this different authorization process does not work when a user is running the app as a webapp. AFAIK it only works from the script editor itself or running directly from a spreadsheet.
If your users are just a known few, you could advise everybody to open the script editor (or a spreadsheet that contains it) and run an specific function that will just attempt the UrlFetchApp.fetch call so the popup shows up and they authorize it. Once this step is done, they can use the webapp normally. Apps Script will do the magic for you after that.
But if you plan to share this broadly, say at the Chrome Web Store, and don't want to ask every user to do this somewhat strange step, then you'll need to manage all the authorization process yourself. It means, you'll have to register your app with the third party service (if it's Google's, it's at the API Console), where you will receive a client id and a client secret. With those you'll have to place a "Authorize" submit button on your app html that will redirect the users to the 3rd party authorization url, providing the correct scope, etc. When they authorize it, the 3rd party will redirect the user back to your app providing a code token as URL parameter. You'll use this code to call the 3rd party oAuth service to get the real access and possibly refresh tokens that you'll have to use on your UrlFetch calls. You'll be responsible to save these tokens, refresh them when they expire and so on. Not a very simple procedure :-/
Oh, and although your app have only one id and secret, the tokens are per user. Which makes sense, since each call you do must be on behalf of a specific user and he *must* have authorized it.
I hope this helps.

UiApp: Persistent login for non-gmail users

I am attempting to implement a Google Apps Script web service which requires users to log in using an account set up on our system.
The users will not necessarily have a gmail account, and should not be required to create one.
The web service must run using as the script owner, as it is necessary for it to be able to write to a spreadsheet and other resources which do not have shared write permission.
I have managed to implement the login screen, with reasonably strong security -- but the problem I encounter now is that users must log back in every time they visit, and even if they hit the refresh button.
Any ideas on how to implement this?
Is there some way to store a cookie in the users browser, containing a session id?
Or is there some other method which can work?
Thanks in advance!
Josh
This is a very old post but as there is a solution, I think it is better to show it to help people with a similar need
Hi Josh,
I have developed such a system and there is indeed a way to do this.
You can indeed develop a cookie like system that is using the PrivateCache class: CacheService.getPrivateCache().
It works if the user reload the page or close it.
However with this solution when you close your browser it will not be possible to retrieve the information anymore.
Here are the functions that I use to prevent the problem you have underlined
Feel free to adapt them
function getCookie(){
var cache=CacheService.getPrivateCache();
var cached=cache.get("UserCookie");
if(cached!=null){
return Utilities.jsonParse(cached);
}
return -1;
}
function createCookie(data){
var cache=CacheService.getPrivateCache();
cache.put("UserCookie",Utilities.jsonStringify(data),1800);
}
function removeCookie(){
var cache=CacheService.getPrivateCache();
cache.remove("UserCookie");
}
Another way would be to use UserProperties. In this case it will work even if you close your browser... I just tried it
the functions to use are therefore:
function getCookie(){
var cached=UserProperties.getProperty('UserCookie');
if(cached!=null){
return Utilities.jsonParse(cached);
}
return -1;
}
function createCookie(data){
UserProperties.setProperty('UserCookie',Utilities.jsonStringify(data));
}
function removeCookie(){
UserProperties.deleteProperty("UserCookie");
}
I hope it will help anyone...
Cheers
Nicolas
Persistent login are not possible with Apps Script as Apps Script can not interact with browser objects like cookies etc. Apps Script is intended to work only with Google Accounts.