Google-api-javascript-client: Accessing service account

Created on 27 Apr 2018  Â·  9Comments  Â·  Source: google/google-api-javascript-client

I'm trying to access (read/write) Google Drive without my users having to log in or seeing any kind of dialog. I simply want to store about 2K of data for a web page and update it as necessary. I don't have access to the server side and have to do everything on the client side. It seems to me that a Google Service account would be the way to do this and I have created one but I don't know how to access it. I think I'm supposed to use gapi.client.init() passing a JWT in some way. I've found a way of generating a JWT but I don't know how to pass it to gapi.

I understand that this won't be terribly secure as my login data will potentially be exposed but I'm willing to accept this as nothing sensitive is stored on that account. If somebody wants to know the upcoming schedule for our bike club ride-outs, they are welcome to it!

Also, when I created the account, it gave me a JSON file. What is that for?

All 9 comments

Here's what you want to use: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject

You can set the JWT as an access_token to gapi.

The JSON file is the private key that you can use to generate the JWT. We don't recommend you putting that JSON file to a webpage for anyone to steal.

Thanks bochunz. Between that and what I had already found, I am now getting a bearer token like this:

`
var pHeader = { "alg": "RS256", "typ": "JWT" }
var sHeader = JSON.stringify(pHeader);

var pClaim = {};
pClaim.aud = "https://www.googleapis.com/oauth2/v3/token";
pClaim.scope = "https://www.googleapis.com/auth/drive";
pClaim.iss = "[email protected]";
pClaim.exp = KJUR.jws.IntDate.get("now + 1hour");
pClaim.iat = KJUR.jws.IntDate.get("now");

var sClaim = JSON.stringify(pClaim);

var key = "-----BEGIN PRIVATE KEY----- blah blah blah \n-----END PRIVATE KEY-----\n";
var sJWS = KJUR.jws.JWS.sign(null, sHeader, sClaim, key);

var XHR = new XMLHttpRequest();
var urlEncodedData = "";
var urlEncodedDataPairs = [];

urlEncodedDataPairs.push(encodeURIComponent("grant_type") + '=' + 
encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"));
urlEncodedDataPairs.push(encodeURIComponent("assertion") + '=' + encodeURIComponent(sJWS));
urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

// We define what will happen if the data are successfully sent
XHR.addEventListener('load', function (event) {

    var response = JSON.parse(XHR.responseText);
    console.log('In auth callback');
    console.log(response); //response looks good
    token = response["access_token"]

});

// We define what will happen in case of error
XHR.addEventListener('error', function (event) {
    console.log('Oops! Something went wrong.');
    console.log(event);
});

XHR.open('POST', 'https://www.googleapis.com/oauth2/v3/token');
XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
XHR.send(urlEncodedData);

`
Now I have a token, what do I do with it? I'm not sure what parts of authorization have already been done.

I want to get here:

`
console.log("Loading drive");

gapi.load('client', function () {

   gapi.client.load('drive', 'v3', function () {

        console.log("Loaded drive");
        var query = "title='" + fileName + "' and trashed = false";
        console.log(query);

        gapi.client.drive.files.list({ q: query, spaces: 'drive' }, function (err, res) {

            console.log("In files list callback");

            if (err) {
                // Handle error
                console.error(err);
                console.log("error in list files");
                callback(false, err, "")
            }

            else {
                console.log(res.items[0]);
                callback(true, res.items[0], res.items[0]);
            }

        });

    });

});

`
But none of those calls seem to want a token. What, if anything do I have to do between the first code block and the second?

BTW, I hear you about the private key not being secure but I have no alternative because I have no access to the server. Nothing sensitive will be stored in this account. As I said, if somebody wants to know the upcoming schedule for our bike club ride-outs, they are welcome to it!

As I mentioned before, you want to use the gapi.client.setToken function, documented here: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject

Many thanks, I didn't realize you were pointing me to that specific function, there's about 50 functions on that page and I didn't actually believe it could be _that_ simple :)

I have now successfully created, written to and read from a file on Drive so I'm a happy bunny and I'm closing this issue.

@TerryR999, thanks for opening this! I've been running around with the exact same issue. I understand exactly what the security risks are, and explain that to everyone who warns me, but I still want to use a service account with the Google JavaScript client library.

I saw that you said you were a happy bunny, but I didn't understand which way you took bochunz's advice. Did you use gapi.client.setToken() directly on the JWT? Or, did you take the response from the initial JWT -> 'https://www.googleapis.com/oauth2/v3/token' POST request and use that as the argument?

For example, was it exhibit A or B of the following?:

exhibit A:

JWT = <correctly_configured_JWT>;
gapi.client.setToken(JWT);

"Happy bunny time! All requests are working now"

exhibit B:

JWT = <correctly_configured_JWT>;
access_token = <response_from_JWT_submission_for_access_token>;
gapi.client.setToken(access_token);

"Happy bunny time! All requests are working now"

I appreciate all the help you can give me! Code snippets welcome and encouraged. :)

Merry Christmas as well! Hope you're enjoying your holidays!

Hi,

I’ve attached a complete I/O system using Google Service Accounts. I guess that counts as a code snippet (for all sufficiently large values of snippet!). All the info. is in the comments. Please feel free to ask if you have any questions. I’m still working on preparing it for publication but it is in daily use on my web site.

NOTE: This code works fine on all desktop browsers but fails on tablets. I’m still working on that.

Terry

//START CODE

//IMPORTANT NOTES:

//1) trio is a file I/O system that stores & retrieves data from a Google Drive

// service account without annoying logon dialogs. The file I/O is completely

// transparent to the end user and they do not need a Google account. It was

// written by Terry Richards ([email protected]) unless it doesn't

// work, in which case I have no idea who wrote it. Seriously though, bug

// reports and comments (even negative ones) are welcome. The Google Drive

// interface is a rat's nest and its documentation is even worse. Some of

// this functionality was found in various obscure corners of the Internet

// which has resulted in some inconsistency of style. Hopefully, I will be

// tidying this up in future releases.

//2) trio files are text files. Packing and unpacking your data into/from a

// character string is your problem. I generally use new lines (n) to

// separate records and some unique series of characters to separate fields

// but you can do it any way you like (XML, for example).

//3) trio knows nothing about directories and doesn't use them. All files are

// stored in, and accessed from, the root.

//4) trio is intended for client-side use when you do not have access to the

// server. For example, additional pages on a bulletin board where the hosting

// company will not let you store or update data files on their server.

//5) Because it is entirely client side, the secret key information is visible

// to anybody with access to a debugger (essentially everybody) that knows

// how to use it (a smaller number but still a lot of people). Assume that

// any data you store is not secure.DO NOT, UNDER ANY CIRCUMSTANCES, USE trio

// TO STORE SENSITIVE DATA OF ANY KIND. Unfortunately, this is unavoidable

// if you do not have access to the server and don't want to see Google logon

// dialogs.

//6) All files in trio are identified by file name which is case-sensitive (i.e.

// File.txt and file.txt are different files). Google Drive allows duplicate

// file names but trio does not. As long as you only use trio, this is not an

// issue. If you access Google Drive by some other method (e.g. gapi) and

// create duplicate file names, bad things will happen.

//7) trio uses the KJUR library to create and sign the JWT for authentication.

//8) You will need to include the following libraries in your HTML page:

//

//

//

//9) You need to set the two following variables. The values can be found in the

// JSON file (that you downloaded when you created the service account). They

// are in the client_email and private_key fields respectively.

var trioIss = "service account [email protected]";

var trioKey = "-----BEGIN PRIVATE KEY-----n BLAH BLAH n-----END PRIVATE KEY-----n";

//Other variables - Do not mess with these.

var trioDriveLoaded = false;

var trioAuthDone = false;

var trioTraceFunc = null;

var trioTokenExpires;

var fnf = "File not Found";

//Helper functions - Do not call these directly.

function trioLoadDrive(callback) {

            if (trioDriveLoaded) {



                            if (trioTraceFunc)

                                            trioTraceFunc("Drive already loaded");



                            callback();



                            return;

}

            if (trioTraceFunc)

                            trioTraceFunc("Loading client");



            gapi.load('client', function () {



                            if (trioTraceFunc)

                                            trioTraceFunc("Loading Drive");



                            gapi.client.load('drive', 'v3', function () {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("Loaded drive");



                                            trioDriveLoaded = true;

                                            callback();



                            });



            });

}

function trioDoAuth(callback) {

            //Check whether Auth has already been done and that the

            //token is still valid. The expiry time has a five minute

            //safety margin built in which should be enough.



            var now = new Date();



            if (trioAuthDone && now < trioTokenExpires) {



                            if (trioTraceFunc)

                                            trioTraceFunc("Auth already done");



                            callback(true,"");

                            return;



            }



            //Either this is the first time or the token is expired.

            //First, make sure the Drive api is loaded



            trioLoadDrive(function () {



                            //The Drive api is loaded, go ahead and get the Auth token



                            if (trioTraceFunc)

                                            trioTraceFunc("In callback from trioLoadDrive");



                            var pHeader = { "alg": "RS256", "typ": "JWT" }

                            var sHeader = JSON.stringify(pHeader);



                            var pClaim = {};

                            pClaim.aud = "https://www.googleapis.com/oauth2/v3/token";

                            pClaim.scope = "https://www.googleapis.com/auth/drive";

                            pClaim.iss = trioIss;

                            pClaim.exp = KJUR.jws.IntDate.get("now + 1hour");

                            pClaim.iat = KJUR.jws.IntDate.get("now");



                            var sClaim = JSON.stringify(pClaim);



                            var key = trioKey;

                            var sJWS = KJUR.jws.JWS.sign(null, sHeader, sClaim, key);



                            var XHR = new XMLHttpRequest();

                            var urlEncodedData = "";

                            var urlEncodedDataPairs = [];



                            urlEncodedDataPairs.push(encodeURIComponent("grant_type") + '=' + encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"));

                            urlEncodedDataPairs.push(encodeURIComponent("assertion") + '=' + encodeURIComponent(sJWS));

                            urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');



                            //Define what will happen if the data is successfully sent



                            XHR.addEventListener('load', function (event) {



                                            var token = JSON.parse(XHR.responseText);



                                            if (trioTraceFunc)

                                                            trioTraceFunc('In auth callback: Token = ' + token);



                                            //Give the token to the Drive API. This enables all the gapi functions



                                            gapi.client.setToken(token);

                                            trioAuthDone = true;

                                            trioTokenExpires = new Date();

                                            trioTokenExpires.setSeconds(trioTokenExpires.getSeconds() + token.expires_in - 300); //Now + token expiry minus 5 minutes



                                            callback(true, "");



                            });



                            //Define what will happen in case of error



                            XHR.addEventListener('error', function (event) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc('Error in Auth: ' + event);



                                            callback(false, "Authorization failed");



                            });



                            XHR.open('POST', 'https://www.googleapis.com/oauth2/v3/token');

                            XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

                            XHR.send(urlEncodedData);



            });

}

function trioGetFileID(fileName, callback) {

            //Retrieves the file ID for an existing file.



            if (trioTraceFunc)

                            trioTraceFunc("Getting file ID");



            var query = "name = '" + fileName + "' and trashed = false";

            var i;



            if (trioTraceFunc)

                            trioTraceFunc("Query: " + query);



            gapi.client.drive.files.list({ 'q': query })



                            .then(function (res) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("In files list callback");



                                            for (i = 0; i < res.result.files.length; ++i) {



                                                            if (res.result.files[i].name == fileName) {



                                                                            if (trioTraceFunc)

                                                                                            trioTraceFunc(res.result.files[0].name + ': ' + res.result.files[0].id);



                                                                            callback(true, res.result.files[i].id);

                                                                            return;



                                                            }



                                            }



                                            //If we get here, there was no exact match



                                            if (trioTraceFunc)

                                                            trioTraceFunc("Exact file not found");



                                            callback(false, fnf);



                            },function (err) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("Caught error from file list: " + err.status);



                                            callback(false, "Error " + err.status + " in gapi.client.drive.files.list in trioGetFileID");



                            });

}

//The main I/O functions. The parameters are (more or less) the same for all:

//fileName: the file name. E.g. 'Events.txt'

//text: the text to be written for trioWriteFile and trioAppendFile.

// Not used for trioReadFile

//callback: a function that is called after the trio function completes.

// In all cases, the callback has the same signature:

// function(success,buffer)

// If success is false, the function has failed and there is no point

// continuing. In this case, buffer will contain a short error message.

// Further information can often be found in the console log.

// If success is true, buffer will contain:

// trioReadFile: The file's content as a single character string

// trioWriteFile: The file's new content. This will be the same as

// the text string you passed in.

// trioAppendFile: The file's new content. This will be the same as

// the old content plus the string you passed in.

// The callback can be a separate function like this:

// function cb(success,buffer){

// ...

// }

// trioWriteFile('foo.txt','rubbage',cb);

// Or it can be written inline (my preference). See trioAppendFile for

// an example. The results are exactly the same with either method.

function trioReadFile(fileName, callback) {

            //Not surprisingly, this function reads the file identified by fileName. 

            //After it has read the file, it calls the callback procedure that  

            //you supply and passes it a string that contains the file's contents

            //or an error message on failure. 



            if (trioTraceFunc)

                            trioTraceFunc("In trioReadFile");



            trioDoAuth(function (success,msg) {



                            if (!success) {



                                            callback(false, msg + " from trioReadFile");

                                            return;



                            }



                            trioGetFileID(fileName, function (success, ID) {



                                            if (!success) {



                                                            callback(false, ID + " from trioReadFile");

                                                            return;



                                            }



                                            gapi.client.drive.files.get({



                                                            'fileId': ID,

                                                            'alt': 'media'



                                            }).then(function (success) {



                                                            if (trioTraceFunc)

                                                                            trioTraceFunc("Get success");



                                                            callback(true, success.body);



                                            }, function (fail) {



                                                            if (trioTraceFunc)

                                                                            trioTraceFunc("Get fail: " + fail);



                                                            callback(false, fail.result.error.message + " in trioReadFile");



                                            });



                            });



            });

}

function trioWriteFile(fileName, text, callback) {

            //Not surprisingly, this function writes to the file identified by fileName. 

            //After it has written the file, it calls the callback procedure that  

            //you supply and passes it a string that contains the file's contents

            //or an error message if it failed. 



            if (trioTraceFunc)

                            trioTraceFunc("In trioWriteFile");



            trioDoAuth(function (success, msg) {



                            if (!success) {



                                            callback(false, msg + " from trioWriteFile");

                                            return;



                            }



                            trioGetFileID(fileName, function (success, ID) {



                                            if (!success) {



                                                            callback(false, ID + " from trioWriteFile");

                                                            return;



                                            }



                                            var xhr = new XMLHttpRequest();

                                            xhr.responseType = 'text';

                                            xhr.onreadystatechange = function () {

                                                            if (xhr.readyState != XMLHttpRequest.DONE)

                                                                            return;

                                                            callback(true,text);

                                            };

                                            xhr.open('PATCH', 'https://www.googleapis.com/upload/drive/v3/files/' + ID + '?uploadType=media');

                                            xhr.setRequestHeader('Authorization', 'Bearer ' + gapi.auth.getToken().access_token);

                                            xhr.send(text);



                            });



            });

}

function trioAppendFile(fileName, text, callback) {

            //There is no way in HTTP to append to a file.

            //So, what we do is read the file, append the new content

            //to the old content and write it back to the same file.



            if (trioTraceFunc)

                            trioTraceFunc("In trioAppendFile");



            trioReadFile(fileName, function (success, buffer) {



                            //This function is called when readFile is finished.

                            //It passes back the current file content.



                            if (!success) {



                                            callback(false, buffer + " from trioAppendFile");

                                            return;



                            }



                            //Append the new content



                            buffer += text;



                            //And write the whole thing back.

                            //We don't need a callback as there is nothing else to do. 

                            //We just pass in the callback provided by our caller.



                            trioWriteFile(fileName, buffer, callback);



            });

}

//Other useful file functions you can call.

//Create a new file on your Drive service account. The callback

//signature is the same as the main IO functions. If it succeeds,

//success will be true and the second parameter will be an empty

//string.If it fails, success will be false and the second

//parameter will contain an error message. This function will not

//create duplicate files.

function trioCreateFile(fileName, callback) {

            if (trioTraceFunc)

                            trioTraceFunc("In trioCreateFile");



            trioCheckFileExists(fileName, function (success, fileExists) {



                            if (trioTraceFunc)

                                            trioTraceFunc("In callback from trioCheckFileExists");



                            if (!success) {



                                            //On failure, fileExists contains an error message

                                            callback(false, fileExists + " from trioCreateFile");

                                            return;



                            }



                            if (fileExists) {



                                            callback(false, "Duplicate file from trioCreateFile");

                                            return;



                            }



                            //If we get here, the file does not exist so we can 

                            //go ahead and create it



                            var fileMetadata = {

                                            'name': fileName,

                                            'mimeType': 'text/plain'

                            };



                            gapi.client.drive.files.create({

                                            resource: fileMetadata,

                                            fields: 'id'

                            }).execute(function (resp, raw_resp) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc('New file Id: ', resp.id);



                                            callback(true, "");



                            });



            });

}

//Delete a file from your Drive service account. The callback

//signature is the same as the main IO functions. If it succeeds,

//success will be true and the second parameter will be an empty

//string.If it fails, success will be false and the second

//parameter will contain an error message.

function trioDeleteFile(fileName, callback) {

            if (trioTraceFunc)

                            trioTraceFunc("In trioDeleteFile");



            trioDoAuth(function (success, msg) {



                            if (trioTraceFunc)

                                            trioTraceFunc("Callback from trioDoAuth");



                            if (!success) {



                                            callback(false, msg + " from trioDeleteFile");

                                            return;



                            }



                            trioGetFileID(fileName, function (success, buffer) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("Callback from trioGetFileID: " + buffer);



                                            if (!success) {



                                                            callback(false, buffer + " from trioDeleteFile");

                                                            return;



                                            }



                                            var request = gapi.client.drive.files.delete({ 'fileId': buffer });



                                            request.execute(function (resp) {



                                                            if (trioTraceFunc)

                                                                            trioTraceFunc("Callback from gapi.client.drive.files.delete");



                                                            /*                                           if (resp.result){



                                                                                                                            if (trioTraceFunc) {

                                                                                                                                            trioTraceFunc(resp.result);



                                                                                                                            callback(false,"Error in delete: " + resp.result);

                                                                                                                            return;



                                                                                                            }

                                                            */

                                                            callback(true, "");



                                            });



                            });



            });

}

//List all files on your Drive service account. The callback

//signature is the same as the main IO functions. If it succeeds,

//success will be true and the second parameter will be a

//string containing the file names separated by CR (n).

//If it fails, success will be false and the second

//parameter will contain an error message.

function trioGetFileList(callback) {

            if (trioTraceFunc)

                            trioTraceFunc("In trioGetFileList");



            trioDoAuth(function (success, msg) {



                            if (!success) {



                                            callback(false, msg + " from trioGetFileList");

                                            return;



                            }



                            if (trioTraceFunc)

                                            trioTraceFunc("Getting files");



                            var query = "trashed = false";

                            var i, buffer = "";



                            if (trioTraceFunc)

                                            trioTraceFunc("Query: " + query);



                            gapi.client.drive.files.list({ 'q': query }).then(function (err, res) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("In files list callback");



                                            if (err) {



                                                            //May not actually be an error - this can get triggered for a 

                                                            //return code of 200 which indicates success.



                                                            if (err.status == 200) {



                                                                            if (trioTraceFunc)

                                                                                            trioTraceFunc("Error 200 treated as success");



                                                                            for (i = 0; i < err.result.files.length; ++i) {



                                                                                            buffer += err.result.files[i].name;

                                                                                            buffer += "\n";



                                                                            }



                                                                            callback(true, buffer);



                                                            }



                                                            else {



                                                                            if (trioTraceFunc)

                                                                                            trioTraceFunc("Error in list files: " + err.status);



                                                                            callback(false, "Error " + err.status + " in trioGetFileList");



                                                            }



                                            }



                                            else { //not err



                                                            if (trioTraceFunc)

                                                                            trioTraceFunc("List files success");



                                                            for (i = 0; i < res.result.files.length; ++i) {



                                                                            buffer += res.result.files[i].name;

                                                                            buffer += "\n";



                                                            }



                                                            callback(true, buffer);



                                            }



                            });



            });

}

//Check whether a file exists on your Drive service account.

//The callback signature is the same as the main IO functions.

//If it succeeds, success will be true and the second parameter

//will be a boolean: true if the file exists and false if it

//doesn't.

//If it fails, success will be false and the second

//parameter will contain an error message.

function trioCheckFileExists(fileName, callback) {

            //The easiest way to see if a file exists is to 

            //try and retrieve its ID. If we get one, the file

            //exists. If we don't, it doesn't.



            if (trioTraceFunc)

                            trioTraceFunc("In trioCheckFileExists");



            trioGetFileID(fileName, function(success,msg) {



                            if (!success) {



                                            if (trioTraceFunc)

                                                            trioTraceFunc("Failed to get file ID");



                                            if (msg == fnf)

                                                            callback(true, false);

                                            else

                                                            callback(false, msg + " from trioFileExists");



                                            return;



                            }



                            if (trioTraceFunc)

                                            trioTraceFunc("Got file ID, file exists");



                            callback(true, true);



            });

}

//Functions that control trio behaviour

function trioSetTrace(newVal) {

            //Turns trace on (newVal=a function that takes a char string) 

            //or off(newVal = null).

            //It is initially off. If you turn it on the logic flow

            //is traced in the provided function. This can generate 

            //a fair amount of output so only use it as necessary for 

            //debugging etc.

            //You can call this at any time and as often as you like.

            //Error information is still logged even if trace is

            //turned off.

            //There is no callback as the function executes immediately.



            trioTraceFunc = newVal;

}

//END CODE

From: DaHogie [mailto:[email protected]]
Sent: mardi 25 décembre 2018 17:03
To: google/google-api-javascript-client
Cc: TerryR999; Mention
Subject: Re: [google/google-api-javascript-client] Accessing service account (#428)

@TerryR999 https://github.com/TerryR999 , thanks for opening this! I've been running around with the exact same issue. I understand exactly what the security risks are, and explain that to everyone who warns me, but I still want to use a service account with the Google JavaScript client library.

I saw that you said you were a happy bunny, but I didn't understand which way you took bochunz's advice. Did you use gapi.client.setToken() directly on the JWT? Or, did you take the response from the initial JWT -> 'https://www.googleapis.com/oauth2/v3/token' POST request and use that as the argument?

For example, was it exhibit A or B of the following?:

exhibit A:

JWT = ;
gapi.client.setToken(JWT);

"Happy bunny time! All requests are working now"

exhibit B:

JWT = ;
access_token = ;
gapi.client.setToken(access_token);

"Happy bunny time! All requests are working now"

I appreciate all the help you can give me! Code snippets welcome and encouraged. :)

Merry Christmas as well! Hope you're enjoying your holidays!

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/google/google-api-javascript-client/issues/428#issuecomment-449860211 , or mute the thread https://github.com/notifications/unsubscribe-auth/Ak--ijCLMcQPYbKM_u9Uop2TuCJeO230ks5u8kwagaJpZM4Tp9a0 . https://github.com/notifications/beacon/Ak--ipY9ugy3cPTKen43oSS_Spm-2v7Pks5u8kwagaJpZM4Tp9a0.gif

@TerryR999, thank you for an enormous Christmas gift! All the best!

No problem. As I said, I’m still working on it so any comments would be welcome J

From: DaHogie [mailto:[email protected]]
Sent: mardi 25 décembre 2018 18:02
To: google/google-api-javascript-client
Cc: TerryR999; Mention
Subject: Re: [google/google-api-javascript-client] Accessing service account (#428)

@TerryR999 https://github.com/TerryR999 , thank you for an enormous Christmas gift! All the best!

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/google/google-api-javascript-client/issues/428#issuecomment-449863223 , or mute the thread https://github.com/notifications/unsubscribe-auth/Ak--igqPfDNoZq1nYWDm4CRGpdtLawd8ks5u8lnqgaJpZM4Tp9a0 . https://github.com/notifications/beacon/Ak--imGdYa0tFp1A2DuNV4dFVWZIj15zks5u8lnqgaJpZM4Tp9a0.gif

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Nilesh-P picture Nilesh-P  Â·  4Comments

lucasriondel picture lucasriondel  Â·  3Comments

caesarsol picture caesarsol  Â·  6Comments

nmquaan-bigbom picture nmquaan-bigbom  Â·  7Comments

bxboxer219 picture bxboxer219  Â·  6Comments