43
votes

I'm trying to authorize my application to integrate with Google Drive. Google documentation provides details for server based authorization and code samples for various server technologies.

There's also a JavaScript Google API library, that has support for authorization. Down in the samples section of the wiki there is a code snippet for creating a config and calling the authorize function. I've altered the scope to be that one I believe is required for drive:

var config = {
    'client_id': 'my_client_ID',
    'scope': 'https://www.googleapis.com/auth/drive.file'
  };
  gapi.auth.authorize(config, function() {
    console.log(gapi.auth);
  });

The callback function is never called (yes, the Google API library is loaded corrected) Looking the Java Retrieve and Use OAuth 2.0 Credentials example, the client secret seems to be a parameter, should this go into the config?

Has anyone tried this in JS, for Drive or other Google APIs? Does anyone know the best route for debugging such a problem, i.e. do I need to just step through the library and stop whinging?

Please don't suggest doing the authorization on the server side, our application is entirely client side, I don't want any state on the server (and I understand the token refresh issues this will cause). I am familiar with the API configuration in the Google console and I believe that and the drive SDK setting are correct.

2
Your github link is stale - can you update it please?Mogsdad

2 Answers

61
votes

It is possible to use the Google APIs Javascript client library with Drive but you have to be aware that there are some pain points.

There are 2 main issues currently, both of which have workarrounds:

Authorization

First if you have a look closely at how Google Drive auth works you will realize that, after a user has installed your Drive application and tries to open a file or create a new file with your application, Drive initiates the OAuth 2.0 authorization flow automatically and the auth parameters are set to response_type=code and access_type=offline. This basically means that right now Drive apps are forced to use the OAuth 2 server-side flow which is not going to be of any use to the Javascript client library (which only uses the client-side flow).

The issue is that: Drive initiates a server-side OAuth 2.0 flow, then the Javascript client library initiates a client-side OAuth 2.0 flow.

This can still work, all you have to do it is use server-side code to process the authorization code returned after the Drive server-side flow (you need to exchange it for an access token and a refresh token). That way, only on the first flow will the user be prompted for authorization. After the first time you exchange the authorization code, the auth page will be bypassed automatically.

Server side samples to do this is available in our documentation.

If you don't process/exchange the auth code on the server-side flow, the user will be prompted for auth every single time he tries to use your app from Drive.

Handling file content

The second issue is that uploading and accessing the actual Drive file content is not made easy by our Javascript client library. You can still do it but you will have to use custom Javascript code.

Reading the file content

When a file metadata/a file object is retrieved, it contains a downloadUrl attribute which points to the actual file content. It is now possible to download the file using a CORS request and the simplest way to auth is to use the OAuth 2 access token in a URL param. So just append &access_token=... to the downloadUrl and fetch the file using XHR or by forwarding the user to the URL.

Uploading file content

UPDATE UPDATE: The upload endpoints do now support CORS.

~~UPDATE: The upload endpoints, unlike the rest of the Drive API do not support CORS so you'll have to use the trick below for now:~~

Uploading a file is tricky because it's not built-in the Javascript client lib and you can't entirely do it with HTTP as described in this response because we don't allow cross-domain requests on these API endpoints. So you do have to take advantage of the iframe proxy used by our Javascript client library and use it to send a constructed multipart request to the Drive SDK. Thanks to @Alain, we have an sample of how to do that below:

/**
 * Insert new file.
 *
 * @param {File} fileData File object to read data from.
 * @param {Function} callback Callback function to call when the request is complete.
 */
function insertFileData(fileData, callback) {
  const boundary = '-------314159265358979323846';
  const delimiter = "\r\n--" + boundary + "\r\n";
  const close_delim = "\r\n--" + boundary + "--";

  var reader = new FileReader();
  reader.readAsBinaryString(fileData);
  reader.onload = function(e) {
    var contentType = fileData.type || 'application/octet-stream';
    var metadata = {
      'title': fileData.fileName,
      'mimeType': contentType
    };

    var base64Data = btoa(reader.result);
    var multipartRequestBody =
        delimiter +
        'Content-Type: application/json\r\n\r\n' +
        JSON.stringify(metadata) +
        delimiter +
        'Content-Type: ' + contentType + '\r\n' +
        'Content-Transfer-Encoding: base64\r\n' +
        '\r\n' +
        base64Data +
        close_delim;

    var request = gapi.client.request({
        'path': '/upload/drive/v2/files',
        'method': 'POST',
        'params': {'uploadType': 'multipart'},
        'headers': {
          'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
        },
        'body': multipartRequestBody});
    if (!callback) {
      callback = function(file) {
        console.log(file)
      };
    }
    request.execute(callback);
  }
}

To improve all this, in the future we might:

  • Let developers choose which OAuth 2.0 flow they want to use (server-side or client-side) or let the developer handle the OAuth flow entirely.
  • Allow CORS on the /upload/... endpoints
  • Allow CORS on the exportLinks for native gDocs
  • We should make it easier to upload files using our Javascript client library.

No promises at this point though :)

14
votes

I did it. Heres my code:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
    <style>
        p {         
            font-family: Tahoma;
        }
    </style>
  </head>
  <body>
    <!--Add a button for the user to click to initiate auth sequence -->
    <button id="authorize-button" style="visibility: hidden">Authorize</button>
    <script type="text/javascript">
      var clientId = '######';
      var apiKey = 'aaaaaaaaaaaaaaaaaaa';
      // To enter one or more authentication scopes, refer to the documentation for the API.
      var scopes = 'https://www.googleapis.com/auth/drive';

      // Use a button to handle authentication the first time.
      function handleClientLoad() {
        gapi.client.setApiKey(apiKey);
        window.setTimeout(checkAuth,1);
      }

      function checkAuth() {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: true}, handleAuthResult);
      }

      function handleAuthResult(authResult) {
        var authorizeButton = document.getElementById('authorize-button');
        if (authResult && !authResult.error) {
          authorizeButton.style.visibility = 'hidden';
          makeApiCall();
        } else {
          authorizeButton.style.visibility = '';
          authorizeButton.onclick = handleAuthClick;
        }
      }

      function handleAuthClick(event) {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false}, handleAuthResult);
        return false;
      }

      // Load the API and make an API call.  Display the results on the screen.
      function makeApiCall() {
        gapi.client.load('drive', 'v2', function() {

          var request = gapi.client.drive.files.list ( {'maxResults': 5 } );

          request.execute(function(resp) {          
            for (i=0; i<resp.items.length; i++) {
                    var titulo = resp.items[i].title;
                    var fechaUpd = resp.items[i].modifiedDate;
                    var userUpd = resp.items[i].lastModifyingUserName;

                    var fileInfo = document.createElement('li');
                    fileInfo.appendChild(document.createTextNode('TITLE: ' + titulo + ' - LAST MODIF: ' + fechaUpd + ' - BY: ' + userUpd ));                
                    document.getElementById('content').appendChild(fileInfo);
            }
          });        
        });
      }
    </script>
    <script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>    
    <p><b>These are 5 files from your GDrive :)</b></p>
    <div id="content"></div>
  </body>
</html>

You only have to change:

  • var clientId = '######';
  • var apiKey = 'aaaaaaaaaaaaaaaaaaa';

to your clientID and ApiKey from your Google API Console :)

Of course you have to create your project on Google API Console, activate the Drive API and activate Google Accounts auth in OAuth 2.0 (really eeeeasy!)

PS: it wont work locally on your PC, it will work on some hosting, and yoy must provide the url from it on the project console :)