I'm trying to make an online service for my Android app using Google Apps Script. The users simple log in to their Google Account inside the app, grant necessary permissions to the corresponding GCP and then the app takes care of obtaining access tokens with a refresh token to call the service on behalf of the user. The main goal is to modify an Spreadsheet on the user's drive. This is the functionality I want to achieve, now comes the technical details.
I created a GCP and a Google Apps Script (GAS) project. In the settings of the GASP, y set the ID of the GCP I created (instead of the default one). On the GCP, I created 2 clients:
In my Android app, I use com.google.android.gms:play-services-auth library to request the user's permission with the necessary OAuth scopes (in this case, https://www.googleapis.com/auth/spreadsheets). The library correctly prompts the dialog to give permission to my app, and the process completes successfully. From here, I use a custom GASP (not linked to the previous GCP) to exchange the authorization code for refresh and access tokens, and to get new access tokens from the refresh token. The app can call this functions without any problem and the exchanges are made correctly against Google's OAuth servers. If I go to my Google account (different from the one that contains the GCP and GASP), I can see the connection to my app, the authorizations correctly being displayed.
Now, I want to execute my function in the GASP, which currently just creates a Spreadsheet on user's Drive, doing a GET request. This is what the actual function looks like:
function doGet(e) {
let r = {}
try {
SpreadsheetApp.create("TEST")
r.status = "ok"
} catch (e) {
r.status = e.message
}
return ContentService.createTextOutput(JSON.stringify(r)).setMimeType(ContentService.MimeType.JSON)
}
I have deployed it as a web app, so that it executes as the user who access it and open to anyone with a Google acconunt.
If I open a browser tab, log in with my authorized Google account and browse to https://script.google.com/macros/s/AKfycbx-sD70nS3Ee518PhbpH6r[...]N1A/exec, the browser displays the JSON with "status":"ok" and the Spreadsheet is created on my Drive. As no error message is prompted, I assume the GASP has the necessary permissions (already granted using my Android app). Then, I wanted to execute this GET request from my Android app, or basically any other place that supports HTTP request. Once I set up an OkHttpClient with an Authenticator and an Interceptor that take care for the authentication, I couldn't execute the function from my app, I always get a 401 error. I simulated my request within the Android app in another GASP (just an environment that allows me to do GET requests):
function request() {
let r = UrlFetchApp.fetch(
"https://script.google.com/macros/s/AKfycbx-sD70nS3Ee518PhbpH6ru_xWgCGr9Cj5pJBCjr[...]N1A/exec",
{
method: "get",
muteHttpExceptions: true,
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
}
)
Logger.log(r.getResponseCode())
Logger.log(r.getContentText())
}
and, unsurprisingly, I get the 401 error, and the context is an HTML from Google saying that I don't have access. So, my specific problem is not with the Android part, but how to authenticate when calling the GET request of my GASP. Just for clarification, I obtained a fresh access token when I did the testing, just to make sure it was not expired. I've searched online and this is the supposed way of doing it, but it simply doesn't work. Any help will be much appreciated!
I figured it out myself. The main problem with my implementation was that GASP deployed as Web apps cannot handle OAuth tokens in the header of an HTTP request, so the alternative is to use an API Executable deployment. This requires the extra OAuth scope https://www.googleapis.com/auth/script.external_request to be asked to the final user and to enable Apps Script API in the GCP associated to the GASP. Once this is set (and the final user has given permission to the app through the OAuth screen which now includes the new scope), one can call the final GASP using a POST HTTP request. For instance, the example of request() function I posted now turns into:
function request() {
let r = UrlFetchApp.fetch(
"https://script.googleapis.com/v1/scripts/AKfycbxlB2OZkA[...]YYEiB44:run",
{
method: "post",
contentType: 'application/json',
muteHttpExceptions: true,
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
payload: JSON.stringify({
'function':'test',
'devMode':false,
'parameters':[]
})
}
)
Logger.log(r.getResponseCode())
Logger.log(r.getContentText())
}
In this case, the URL is obtained directly from the deployment screen (set as execute as Anyone with a Google account), and this specific snippet executes the function test() in my final, authorized script. This function is just the same as the doGet(e) I showed in the question, but it accepts no parameters and returns a String (as JSONs objects are not allowed return types in GASP API calls). That way, now I can call any function I want in behalf of the users using the OAuth token.