24 Jan 2017 : Checking the user's email address in a chrome extension: part two

A few days ago I posted about how to get the email address for a Google Calendar user from inside a Chrome extension. It turns out that that’s not the whole story.

chrome.identity.getProfileUserInfo doesn’t return the account that’s logged into Calendar; it’s the account that’s logged into Chrome. That’s the “Sign in to Chrome” option in Chrome’s settings that syncs bookmarks, history, etc. For most people, that’s probably the same user, but it might not be, or of course you might not be using that setting at all.

So I took a second pass at it and used oauth2 to get the actually-logged-in user. After spending kind of a long time figuring out how to do it, I discovered that… it also returns the user logged into Chrome. Haha, what? But it was an educational journey, andI’ll reproduce the steps here, and hope they’re helpful for someone else.

Btw, the way I ended up getting the email address for the user logged into Calendar is pulling it out of the DOM with

dom_user = $("#onegoogbar .gb_vb").text();

But I’m still looking for a better way to do it. (For people who, like me, are just learning Javascript: that’s JQuery, but you don’t need to use JQuery. Look for an element with id=”onegoogbar” and then class=”gb_vb”.)

Ok, Google Oauth: here’s what worked for me:

1) You need to upload your extension to the Chrome web store.

Yeah, even though it’s not ready yet. You can publish it as ‘private’.

  • Zip up your extension. On linux that’s zip -r out.zip * On not-linux, do whatever you need to do to create a zip file.

  • Go to https://chrome.google.com/webstore/developer/dashboard and upload the zip file.

  • Choose a category and language. You’ll also need a logo. Feel free to use https://github.com/whereistanya/one-to-none/blob/master/logo.png as a placeholder for now so you don’t have yet another side quest to complete.

  • Click “Publish changes”

  • Back at the Developer dashboard, the “Your listings” section should now show your extension. Click “More info” and save the public key and the Item ID. You’ll need them both later.

2) Now you need to create a project at the Google APIs console.

  • Go to https://console.developers.google.com and click “Create project” (It’s beside the “Google APIs” logo at the top.) Give the project a name.

  • It’ll offer you a bunch of APIs. Choose Google+ API. That’s the one that has the identity magic in it.

  • Then it’ll take you to a Traffic/Errors page with a credentials button. There are a bunch of options. You want an “Oauth client ID”. The application type is “Chrome app”. The “application id” is the Item ID you saved above. Yeah, there’s a strange space in the form field, and it doesn’t want the app name even though it looks like it might. Go with it. Save it. It’ll give you a client_id. Note the client_id.

3) Back to your app!

manifest.json

The manifest file needs the “identity” permission and the “email” scope as well as the client_id and and the public key you noted earlier. Mine looks like:

{
"name":"One To None",
"description":"Forgetting to add the other person in your two person meeting",
"version":"1.2",
"icons": {
        "16": "logo.png",
        "48": "logo.png",
        "128": "logo.png"
    },
"manifest_version":2,
"content_scripts": [
    {
      "matches": ["*://calendar.google.com/*"],
      "js": ["jquery-3.1.1.min.js", "content.js"]
    }
  ],
"background": {
  "scripts": ["background.js"],
  "persistent": false
  },
"oauth2": {
  "client_id": "308880467272-2vr6bm5gjb7t9jrtfhb7aqxkenxxx9mi.apps.googleusercontent.com",
  "scopes": [
    "email"
  ]
},
"permissions": [
  "identity"
  ],
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnfWSMjjruQN2+RpLT4L0QFQaL1t5WjjIwNMOpbWNc7FLpZblwAnJBbCZ4pgnfYUlUfsfKiKmNaXbjHmL77Juev6CgTpMThBboTpv3abqSWjCjshwDZ7dDOC0ZJhaqJjpbJeegVgZZxU4S633+71Ztu2fCj1VllDhUtLe9n39uS+sSbW0fFRV8KoxrePONnxY9ou/f1iCw8UOI5wD5kkCPGPM0aotFYu8QhE2XttKOceFk1DVUb02REkxtWc3CwiSGGxxxvdV3jMe5m1tpFFspd2CDZPXafc0elnUX2+6vnHJ1CEtFxIs5hYkZtILr7xMU2zM8/0fsjxHuxTPpSw6CwIDAQAB"
}

background.js

  • The background script does the API wrangling. It needs to:
    • get an oauth access token
    • use that to request the “https://www.googleapis.com/plus/v1/people/me” endpoint
    • pull the email address out of the response
    • and then add a listener for the content script to return the email address.

I read a lot of sites about how to do this. The one I found clearest was https://github.com/GoogleChrome/chrome-app-samples/tree/master/samples/identity, and you’ll notice my code… uh… pays homage to that one :-)

/*** Make a XMLHttpRequest to the identity endpoint ***/

function xhrWithAuth(callback) {
  var access_token;
  var retry = true;
  var url = 'https://www.googleapis.com/plus/v1/people/me';
  getToken();

  /*** Get the access token and call the identity API ***/
  function getToken() {
      chrome.identity.getAuthToken({ interactive: false }, function(token) {
        if (chrome.runtime.lastError) {
          callback(chrome.runtime.lastError);
          return;
        }
        access_token = token;

        var xhr = new XMLHttpRequest();
        xhr.open('get', url);
        xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
        xhr.onload = requestComplete;
        xhr.send();
      });
    }

    /*** Clean up and report any errors ***/
    function requestComplete() {
      if (this.status == 401 && retry) {
        retry = false;
        chrome.identity.removeCachedAuthToken({ token: access_token },
                                              getToken);
      } else {
        callback(null, this.status, this.response);
      }
    }
}

/*** If we got user information, parse out the email address and log it ***/
function onUserInfoFetched(error, status, response) {
  if (!error && status == 200) {
    console.log(response);
    var user_info = JSON.parse(response);
    if (user_info.emails) {
      for (i = 0; i < user_info.emails.length; i++) {
        emails.push(user_info.emails[i].value);
      }
      console.log("Found emails:", emails);
    }
  } else {
    console.log("Error:", error);
  }
}

/*** Return the email addresses when content.js asks for it ***/
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
  console.log("Sending emails:", emails);
  sendResponse( {emails: emails})
});


/*** Main ***/
var emails = [];
xhrWithAuth(onUserInfoFetched);

content.js

  • And finally, the content script needs to request the emails and do whatever it wants to do with them.
var dom_user;
var profile_user;

/*** Get the logged in user from the background.js script. ***/
chrome.extension.sendMessage({}, function(response) {
  if (response.emails) {
    profile_user = response.emails[0].split('@')[0];
    console.log("Got profile user:", profile_user);
  } else {
    console.log("Couldn't get email address of profile user.");
  }
});

/*** Also try scraping the user out of the DOM. This is kind of a hack. ***/
dom_user = $("#onegoogbar .gb_vb").text().split('@')[0];
console.log("Got user from DOM:", dom_user);

Note those last two lines! As I mentioned above, the Google API returned the user logged into Chrome, which isn’t always the same as the one logged into Calendar, so I ended up hacking the email address out of the DOM as well. I doubt this is a stable way to do it, but for now I have two ways that mostly work, and that’s better than nothing :-)

Caveats

My usual Javascript (and actually most things) caveat holds here: I’m figuring this out as I go along and I have no idea what I’m doing. This worked for me, but if any of this seems wrong, it may well be. Feel free to let me know and I’ll fix it.

And I’d also love to hear if it turns out to be useful for anyone else.

For the search index, some errors I got along the way:

  • OAuth2 request failed: Service responded with error: 'bad client id: * APP_ID_OR_ORIGIN_NOT_MATCH'

I didn’t have the public key in the manifest.json. I had to copy it from under “More info” at https://chrome.google.com/webstore/developer/dashboard.

  • bad client id: NON_NATIVE'

When adding the project to the Google APIs Console at https://console.developers.google.com, I added it as a “Web application” and it should have been a “Chrome app”

  • Chrome.identity not available/undefined

The APIs can’t be used in a content script; they have to be in a background page. That’s the ‘background’ section of the manifest.json instead of the content_scripts section.

If you want to see the rest of the code, check out https://github.com/whereistanya/one-to-none.

And the extension is at https://chrome.google.com/webstore/detail/one-to-none/dodlhpgnhejhchfigibbcfddamjjjlem.

Comments