36

I'm trying to access (CRUD) Google Drive from a Firefox extension. Extensions are coded in Javascript, but neither of the two existing javascript SDKs seem to fit; the client-side SDK expects "window" to be available, which isn't the case in extensions, and the server-side SDK seems to rely on Node-specific facilities, as a script that works in node no longer does when I load it in chrome after running it through browserify. Am I stuck using raw REST calls? The Node script that works looks like this:

var google = require('googleapis');
var readlineSync = require('readline-sync');

var CLIENT_ID = '....',
    CLIENT_SECRET = '....',
    REDIRECT_URL = 'urn:ietf:wg:oauth:2.0:oob',
    SCOPE = 'https://www.googleapis.com/auth/drive.file';

var oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);

var url = oauth2Client.generateAuthUrl({
  access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token)
  scope: SCOPE // If you only need one scope you can pass it as string
});

var code = readlineSync.question('Auth code? :');

oauth2Client.getToken(code, function(err, tokens) {
  console.log('authenticated?');
  // Now tokens contains an access_token and an optional refresh_token. Save them.
  if(!err) {
    console.log('authenticated');
    oauth2Client.setCredentials(tokens);
  } else {
    console.log('not authenticated');
  }
});

I wrap the node GDrive SDK using browserify on this script:

var Google = new function(){
    this.api = require('googleapis');
    this.clientID = '....';
    this.clientSecret = '....';
    this.redirectURL = 'urn:ietf:wg:oauth:2.0:oob';
    this.scope = 'https://www.googleapis.com/auth/drive.file';
    this.client = new this.api.auth.OAuth2(this.clientID, this.clientSecret, this.redirectURL);
  }
}

which is then called using after clicking a button (if the text field has no code it launches the browser to get one):

function authorize() {
  var code = document.getElementById("code").value.trim();

  if (code === '') {
    var url = Google.client.generateAuthUrl({access_type: 'offline', scope: Google.scope});
    var win = Components.classes['@mozilla.org/appshell/window-mediator;1'].getService(Components.interfaces.nsIWindowMediator).getMostRecentWindow('navigator:browser');
    win.gBrowser.selectedTab = win.gBrowser.addTab(url);
  } else {
    Google.client.getToken(code, function(err, tokens) {
      if(!err) {
        Google.client.setCredentials(tokens);
        // store token
        alert('Succesfully authorized');
      } else {
        alert('Not authorized: ' + err); // always ends here
      }
    });
  }
}

But this yields the error Not authorized: Invalid protocol: https:

retorquere
  • 1,496
  • 1
  • 14
  • 27
  • 2
    @friflaj Did you tried to mimic a window obj ? `window = Window = {}` form a high level it looks like they only use the winodw obj to store global variables – yoelp Sep 24 '14 at 15:34
  • That's what I'm trying now. The Node version uses a form of dynamic module loading that appears to escape browserify, so I've given up on that. – retorquere Sep 26 '14 at 13:48
  • Have you tried using an iFrame? I believe in firefox that you have access to a "background script". This is basically a headless html page that your code gets executed on. You should be able to generate an iFrame here. (just spitballing, I've gotten this to work before, but can't recall how) – 190290000 Ruble Man Oct 02 '14 at 17:38
  • I wonder if this could be helpful: http://stackoverflow.com/questions/8915087/loading-external-js-to-extend-firefox-extension – 190290000 Ruble Man Oct 02 '14 at 17:39
  • I'm going to try for the iframe option, I'll post back here when I get it to work. The other option would technically work, but I'f have to monkey patch the existing libs (fragile), and as that thread notes, *huge* security hole -- and this is chrome code, which is not sandboxed. – retorquere Oct 02 '14 at 21:31
  • this article might help: https://blog.mozilla.org/addons/2014/04/10/changes-to-unsafewindow-for-the-add-on-sdk/ did you try "unsafeWindow" instead of "window"? – Niczem Olaske Dec 18 '14 at 07:14
  • 6
    I just wanted to add a warning that you shouldn't be attempting to do this fully client-side, since that would involve (as you posted above) exposing your client secret in the source code of your add-on (which is accessible through the filesystem). This poses a security risk to users of the add-on. Instead, I would host a small app on a separate server that your add-on can call, which would authenticate users with the standard OAuth method (callback URL's and such). – Alfred Xing Dec 20 '14 at 02:41
  • @NiczemOlaske I think that only concerns web javascript, I'm running in privileged (chrome) mode. – retorquere Dec 22 '14 at 22:34
  • @AlfredXing I know that's the only safe option if I'm stuck with OAuth-dependent APIs, but I'd rather not maintain a server in addition to my client-side code. OAuth really isn't a good fit for non-web apps (closed source or not), but that's where's everyone heading unfortunately. – retorquere Dec 22 '14 at 22:37
  • could this be a cross-domain policy issue? Try exposing your app to an https URL with [ngrok](https://ngrok.com/) – code_monk Dec 28 '14 at 06:10
  • Hey man did you make any progress on this? – Noitidart Dec 29 '14 at 04:03

2 Answers2

1

From here https://developer.mozilla.org/en/docs/Working_with_windows_in_chrome_code you could try window = window || content || {}

Use the JavaScript client API and not the node.js client. Although browserify will make it work. You will have to expose your client secret in the latter. The flow of client side authentication is very diff than server side. Refer to https://developers.google.com/accounts/docs/OAuth2

Having said all this. Its really not that difficult to implement an app with REST based calls. The methods in all client libraries mimic the corresponding REST URLs. You could set up some functions of your own to handle request and response and the rest would feel the same.

Gaurav Ramanan
  • 3,655
  • 2
  • 21
  • 29
1

It is possible though, depending on the use case, it might also of limited interest.

Firefox ships with a tiny http server, just the bare bones. It is included for test purposes but this is not a reason to overlook it.

Lets follow the quickstart guide for running a Drive app in Javascript

The tricky part is to set the Redirect URIs and the Javascript Origins. Obviously the right setting is http://localhost, but how can you be sure that every user has port 80 available?

You can't and, unless you have control over your users, no port is guaranteed to work for everyone. With this in mind lets choose port 49870 and pray.

So now Redirect URIs and the Javascript Origins are set to http://localhost:49870

Assuming you use Add-on SDK, save the quickstart.html (remember to add your Client ID) in the data directory of your extension. Now edit your main.js

const self = require("sdk/self");
const { Cc, Ci } = require("chrome");
const tabs = require("sdk/tabs");
const httpd = require("sdk/test/httpd");

var quickstart = self.data.load("quickstart.html");

var srv = new httpd.nsHttpServer();

srv.registerPathHandler("/gdrive", function handler(request, response){
  response.setHeader("Content-Type", "text/html; charset=utf-8", false);

  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  response.write(converter.ConvertFromUnicode(quickstart));
})

srv.start(49870);

tabs.open("http://localhost:49870/gdrive");

exports.onUnload = function (reason) {
  srv.stop(function(){});
};

Notice that quickstart.html is not opened as a local file, with a resource: URI. The Drive API wouldn't like that. It is served at the url http://localhost:49870/gdrive. Needless to say that instead of static html we can use a template or anything else. Also the http://localhost:49870/gdrive can be scripted with a regular PageMod.

I don't consider this a real solution. It's just better than nothing.

paa
  • 5,048
  • 1
  • 18
  • 22
  • As it happens I'm doing this for a zotero extension,... and zotero ships with a serviceable http server embedded, so this ought to work. I had not expected google to accept localhost redirect URIs – retorquere Feb 14 '15 at 00:22