20

I can't get a user's id_token (https://developers.google.com/accounts/docs/CrossClientAuth) from the Chrome identity api (https://developer.chrome.com/apps/identity).

I can get an access_token using the chrome identity sample when the oauth section in the manifest is:

"oauth2": {
  "client_id": "<chrome-app-client-id>.apps.googleusercontent.com",
  "scopes": ["https://www.googleapis.com/auth/plus.login"]
}

But when I try to get the id_token the same way I get it on my android client a get the error:

"OAuth2 request failed: Service responded with error: 'invalid scope: {0}'"}

The manifest section is now:

"oauth2": {
  "client_id": "<chrome-app-client-id>.apps.googleusercontent.com",
  "scopes": ["audience:server:client_id:<app-engine-client-id>.apps.googleusercontent.com"]
}

On Android I get the id_token by passing the same scope string to android.gms.auth.GoogleAuthUtil.getToken(), but I can't get it to work with the chrome identity api.

Is it possible to get an id_token with Chrome App Indentity Api? If not, how can I get an id_token for my Chrome app?

Thanks for your help!

Piotr Sobiech
  • 452
  • 4
  • 11
chris
  • 201
  • 2
  • 5
  • 1
    The terminology with OAuth is crazy. In your post, you refer to "user id", "client_id", "id token", and "id". Can you be precise about what is it exactly that you are trying to get? (I know a few things about a "client_id", "id" is too generic for me to understand, and I'm not sure what a "user id" is.) – Marc Rochkind Oct 08 '14 at 19:46
  • Agree, there are a few terms you need to keep separate. Edited the post for clarity. The client_id is mentioned on the first link in the post, it's needed to specify the audience in the id_token. I think it's clear that I want an id_token that can be verified when appended to https://www.googleapis.com/oauth2/v1/tokeninfo?id_token= – chris Oct 09 '14 at 06:38
  • Have you tried adding `openid` to the scopes? This is usually a required hint to get the server into OpenID Connect mode and issue an ID Token. – Pieter Ennes Jan 23 '18 at 10:43

3 Answers3

19

I've came to the same problem yesterday and since I've found a solution, I might as well share it, as it wasn't that obvious. As far as i know Google does not provide a direct and documented way to do this, but you can use the chrome.identity.launchWebAuthFlow() function.

First you should create an Web application credentials in google console and add the following url as a valid Authorized redirect URI: https://<EXTENSION_OR_APP_ID>.chromiumapp.org. The URI does not have to exist, chrome will just catch the redirect to this URL and call your callback function later.

manifest.json:

{
  "manifest_version": 2,
  "name": "name",
  "description": "description",
  "version": "0.0.0.1",
  "background": {
    "scripts": ["background.js"]
  },
  "permissions": [
    "identity"
  ],
  "oauth2": {
    "client_id": "<CLIENT_ID>.apps.googleusercontent.com",
    "scopes": [
      "openid", "email", "profile"
    ]
  }
}

background.js:

// Using chrome.identity
var manifest = chrome.runtime.getManifest();

var clientId = encodeURIComponent(manifest.oauth2.client_id);
var scopes = encodeURIComponent(manifest.oauth2.scopes.join(' '));
var redirectUri = encodeURIComponent('https://' + chrome.runtime.id + '.chromiumapp.org');

var url = 'https://accounts.google.com/o/oauth2/auth' + 
          '?client_id=' + clientId + 
          '&response_type=id_token' + 
          '&access_type=offline' + 
          '&redirect_uri=' + redirectUri + 
          '&scope=' + scopes;

chrome.identity.launchWebAuthFlow(
    {
        'url': url, 
        'interactive':true
    }, 
    function(redirectedTo) {
        if (chrome.runtime.lastError) {
            // Example: Authorization page could not be loaded.
            console.log(chrome.runtime.lastError.message);
        }
        else {
            var response = redirectedTo.split('#', 2)[1];

            // Example: id_token=<YOUR_BELOVED_ID_TOKEN>&authuser=0&hd=<SOME.DOMAIN.PL>&session_state=<SESSION_SATE>&prompt=<PROMPT>
            console.log(response);
        }
    }
);

Google OAuth2 API (for OpenID Connect) documentation can be found here: https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters

PS: If you don't need the oauth2 section in your manifest. You can safely omit it, and provide the identifiers and scopes in code only.

EDIT: For those interested, you don't need the identity API. You can even access the token using a little trick with tabs API. The code is a little longer, but you have better error messages and control. Keep in mind that in the following example, you need to create Chrome App credentials.

manifest.json:

{
  "manifest_version": 2,
  "name": "name",
  "description": "description",
  "version": "0.0.0.1",
  "background": {
    "scripts": ["background.js"]
  },
  "permissions": [
    "tabs"
  ],
  "oauth2": {
    "client_id": "<CLIENT_ID>.apps.googleusercontent.com",
    "scopes": [
      "openid", "email", "profile"
    ]
  }
}

background.js:

// Using chrome.tabs
var manifest = chrome.runtime.getManifest();

var clientId = encodeURIComponent(manifest.oauth2.client_id);
var scopes = encodeURIComponent(manifest.oauth2.scopes.join(' '));
var redirectUri = encodeURIComponent('urn:ietf:wg:oauth:2.0:oob:auto');

var url = 'https://accounts.google.com/o/oauth2/auth' + 
          '?client_id=' + clientId + 
          '&response_type=id_token' + 
          '&access_type=offline' + 
          '&redirect_uri=' + redirectUri + 
          '&scope=' + scopes;

var RESULT_PREFIX = ['Success', 'Denied', 'Error'];
chrome.tabs.create({'url': 'about:blank'}, function(authenticationTab) {
    chrome.tabs.onUpdated.addListener(function googleAuthorizationHook(tabId, changeInfo, tab) {
        if (tabId === authenticationTab.id) {
            var titleParts = tab.title.split(' ', 2);

            var result = titleParts[0];
            if (titleParts.length == 2 && RESULT_PREFIX.indexOf(result) >= 0) {
                chrome.tabs.onUpdated.removeListener(googleAuthorizationHook);
                chrome.tabs.remove(tabId);

                var response = titleParts[1];
                switch (result) {
                    case 'Success':
                        // Example: id_token=<YOUR_BELOVED_ID_TOKEN>&authuser=0&hd=<SOME.DOMAIN.PL>&session_state=<SESSION_SATE>&prompt=<PROMPT>
                        console.log(response);
                    break;
                    case 'Denied':
                        // Example: error_subtype=access_denied&error=immediate_failed
                        console.log(response);
                    break;
                    case 'Error':
                        // Example: 400 (OAuth2 Error)!!1
                        console.log(response);
                    break;
                }
            }
        }
    });

    chrome.tabs.update(authenticationTab.id, {'url': url});
});
Piotr Sobiech
  • 452
  • 4
  • 11
  • Hi Piotr. I've tried your solution but I'm getting *Authorization page could not be loaded* error. I'm doing exactly what you are doing, with the same scopes in the manifest. – Egidio Caprino Jun 12 '16 at 16:50
  • Hi @Aegidius, did you figure this out? I'm having the same issue. Thanks. – rsb Mar 21 '17 at 13:05
  • @Aegidius, same here, not sure if this method is still valid? – seesoe Oct 05 '17 at 21:18
  • 3
    Two changes need to be made to this. 1) New endpoint base is `https://accounts.google.com/o/oauth2/v2/auth` 2) `nonce` is a required URL parameter for the authentication URL, so add something like `&nonce=` – dadykhoff Feb 07 '19 at 17:05
  • 1
    `id_token` is recommended when communicating user information from an extension to a backend server (outlined here: https://developers.google.com/identity/sign-in/web/backend-auth). I cannot for the life of me get this working, though, since I get the message "Invalid Redirect: domain must be added to the authorized domains list before submitting." when attempting to create an OAuth Web Client from the credentials console. Adding the extension's URL to authorized domains does not work because the domains "Must be a top private domain." Seems like a Catch-22... not sure what to do. – chas May 04 '19 at 13:48
  • I did actually get the tabs hack working (thanks Piotr), but that UX will not work for my purposes. – chas May 04 '19 at 13:50
  • The background page opening a new tab worked, thank you!! Seems like my page_action could not redirect itself to the login page – neaumusic Dec 31 '19 at 22:43
  • this solves my problem very well!! echoing @dadykhoff, the url has to be updated and we need to add nonce – rendybjunior Mar 20 '20 at 13:26
0

First, I assume that in your manifest.json snippet you don't mean that your client_id is literally "<chrome-app-client-id>.apps.googleusercontent.com. It should be something like 9414861317621.apps.googleusercontent.com -- something you got from the Developer Console, or whatever Google site you used to register the app.

Assuming the above is OK, and you have the client_id right, and the scope right, you get what's called an "OAuth2 access token" with a call to chrome.identity.getAuthToken. As you don't show us any JavaScript code, I can't tell if this is what you're doing. The access token you get you need to save for subsequent use when you call an API function. For example:

var access_token;

chrome.identity.getAuthToken(
    {
        'interactive': true
    },
    function(token) {
        access_token = token;
        // do something if you like to indicate
        // that the app is authorized
    }
);

Then, when you make an API call, you supply that access token, like this:

var url = 'https://www.googleapis.com/' + method;
Ajax.ajaxSend(url, "json",
    function (status, response) {
        if (response && response.error && response.error.message)
            errorCallback(response.error.message);
        else if (status == 200)
            successCallback(response);
        else
            errorCallback('Result code: ' + status);
    },
    function (e) {
        if (errorCallback)
            errorCallback('Communication error');
    },
    {
        Authorization: 'Bearer ' + access_token
    }
);

Ajax.ajaxSend is my own function:

var Ajax = (function () {
    var api = {
        ajaxSend: function (url, responseType, successCallback, errorCallback, headers) {
            var req = new XMLHttpRequest();
            req.onload = function (e) {
                successCallback(req.status, req.response);
            };
            req.onerror = errorCallback;
            req.responseType = responseType ? responseType : "text";
            req.open("get", url);
            if (headers)
                for (var v in headers)
                    req.setRequestHeader(v, headers[v]);
            req.send();
        }
    };
    return api;
})();

The other undefined functions also are what you'd expect. The third argument to Ajax.ajaxSend is a header to be sent along. (Sorry, I don't have time to develop standalone code just for this answer.)

I hope the above is useful.

Marc Rochkind
  • 3,678
  • 3
  • 30
  • 38
  • 2
    Thanks for trying to help, but this is not an answer to my question. In my question on line 3 I say that I know how to get an access_token (I included that to avoid answers explaining how to get one). What I'm trying to get is an id_token (a token that can be verified when appended to this url: "googleapis.com/oauth2/v1/tokeninfo?id_token=" ) – chris Oct 15 '14 at 11:02
  • 1
    Did you ever figure this out? I'm also trying to do the same. Thanks. – rsb Mar 21 '17 at 03:09
  • 1
    still struggling with this :/ – kidCoder Oct 28 '17 at 22:51
0

I guess it depends on why one would want token id, but in my case access_token was enough to authorize user - by pulling user info from https://www.googleapis.com/oauth2/v2/userinfo?alt=json ( with Authorization header = access_token).

Be Kind
  • 4,712
  • 1
  • 38
  • 45
  • `id_token` can be passed to a server saying "yes, I am this user -- google says I am, and that I intend to use this approval for your app (the project client_id), and you can verify the claim using google's public .well-known certificate" – neaumusic Dec 31 '19 at 22:55