Google-api-javascript-client: Get ID Token from Access Token within a Chrome Extension

Created on 12 Feb 2019  路  13Comments  路  Source: google/google-api-javascript-client

I'm attempting to get an id_token from the access_token in a chrome extension context.

I'm using both the standalone Google Javascript Library and the chrome.identity Chrome Extension API. This is due to the fact that the Google JavaScript library cannot be included on internal chrome extension HTML pages due to an Invalid Cookie Policy (explained further down below).

My current flow is as follows:

My manifest.json has the following keys:

"oauth2": {
  "client_id": "<clientid>.apps.googleusercontent.com",
    "scopes": [
    "openid",
    "profile",
    "email"
  ]
},
"key": "<key from installed version>"

Then in my signin.html page I have the following script:

chrome.identity.getAuthToken({
  interactive: true
}, // Interactive popup, call back below called upon success
(token) => {
  if (!token) {
    return;
  }
  const tokenObj = {
    access_token: token
  };
  gapi.client.setToken(tokenObj);
  gapi.auth.setToken(tokenObj);
  gapi.client.request({
    path: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
    client_id: GAPI_CLIENT_ID
  }).then((response) => { // this works and returns a valid profile
    const profile = response.result;
    populateProfile(profile);
  });
});

I am able to successfully get token from the chrome.identity API. I can manually set the token within GAPI to the one retrieved, and then use to perform valid requests.

Unfortunately I cannot call the init functions for gapi.client or gapi.auth - this is due to Chrome Extension html pages having an "Invalid cookiePolicy", see screenshot below.

screen shot 2019-02-12 at 11 43 26 am

I am still able to somewhat use the APIs, though, by manually setting the token using google.client.setToken({access_token:"<token>"}).

Theoretically you should be able to get the ID token using gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().id_token - however, getAuthInstance() returns undefined because we did not instantiate gapi.auth2 with init.

There's a bit of discussion online about this, including this StackOverflow question.

I've also tried the following code, which does not use the JavaScript library, but also fails:

// 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);
        }
    }
);

It fails with the following error:

Authorization page could not be loaded.

My goal is to get an ID token that I can use to verify their identity and pass on to my back end. Is there a way to get it from the access token using the JavaScript API? Since I've already done an interactive sign in flow with the chrome extension it would also be ideal to not have another popup.

1) Is an access token not more permissive than an ID token? Should I not just be able to directly request/create an ID token from this access token?

2) Is there functionality within the gapi context to retrieve an id token? Specifically I'd like to call gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().id_token without going through gapi.auth2.init, due to the inability to do so on a chrome extension.

3) Is there a flaw in my current reasoning or architecture? I might be misunderstanding best practices or usages with oauth2.

3) Ancillary question, but is there a way to circumvent the Invalid Cookie Policy and get full access to the gapi context from within a Chrome Extension?

Most helpful comment

The google docs could definitely use an update regarding auth privileges and procedures.

It seems getting an auth token for scopes: ["profile identity email"] cannot provide a JWT id_token, and that we'd need to 'Sign In' via a completely different flow

As mentioned in the original post, gapi doesn't work locally because of "Invalid Cookie Policy" from chrome-extension:// domain

Overall, auth is still confusing, and these pages have too too many articles:
https://developers.google.com/identity/protocols/OAuth2
https://developers.google.com/identity/sign-in/web/sign-in
https://developer.chrome.com/extensions/app_identity
https://developer.chrome.com/apps/app_identity

All 13 comments

You're getting the Authorization page could not be loaded. error due to 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=<your nonce value>.

I've been fighting this exact same issue recently, except that I am able to generate id_tokens, but my API still rejects them as it is behind Google Identity Aware Proxy which provides no reason for the rejection, so I'm not sure the tokens are generated correctly.

Thanks! I've updated the code as it is below but am still getting the same error.

// 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(chrome.identity.getRedirectURL('oauth2'));

var url = 'https://accounts.google.com/o/oauth2/v2/auth' + 
          '?client_id=' + clientId + 
          '&response_type=id_token' + 
          '&access_type=offline' + 
          '&nonce=testnonce' + 
          '&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);
        }
    }
);

The client ID I'm passing along is a "Chrome Extension" one. Would that make a difference (that is, it's Chrome Extension rather than Web Client).

Yes, this method requires the OAuth2 client to be 'Web application'. Chrome App is used for the Identity API

Ah that's my issue, thanks!

Unfortunately I need the Chrome App token for the built in identity API. I don't think it's a good user flow to have two different authorization popups so I'm going to need to find an alternate method of verification.

Thanks!

Hi, I did not quite understand. You can get id token?
I have the same problem, and i can't fix it.

I am having the same issue. Basically, I need to register a user with my own backend after they have signed-in via the identity API in my extension. The recommended procedure for this seems to be outlined here, which says to use the JWT returned in id_token for secure communication with the backend. I cannot for the life of me get ahold of id_token. I am starting to not think it's possible.

The google docs could definitely use an update regarding auth privileges and procedures.

It seems getting an auth token for scopes: ["profile identity email"] cannot provide a JWT id_token, and that we'd need to 'Sign In' via a completely different flow

As mentioned in the original post, gapi doesn't work locally because of "Invalid Cookie Policy" from chrome-extension:// domain

Overall, auth is still confusing, and these pages have too too many articles:
https://developers.google.com/identity/protocols/OAuth2
https://developers.google.com/identity/sign-in/web/sign-in
https://developer.chrome.com/extensions/app_identity
https://developer.chrome.com/apps/app_identity

For anyone looking for a potentially answer to this. Check out this stackoverflow answer https://stackoverflow.com/a/32548057/5091531

Note that two changes need to be made to this answer:
1) Change the endpoint base to https://accounts.google.com/o/oauth2/v2/auth
2) Add a nonce to the URL parameters, so add something like &nonce=

@PabiGamito wouldn't including the key in url be dangerous because other extensions listening for tabs would know the private key? or is it just assumed that you're the first to use it and doesnt matter after that?

@neaumusic What are you referring to exactly by 'private key'? The client_id? Because if so, I am pretty sure that is fine to be public.

See https://developers.google.com/identity/sign-in/web/sign-in where the client_id is displayed publicly.

@PabiGamito not the request, but that method works due to a redirect happening where google's signed id_token of the user is in the querystring params or tab.title

@neaumusic Yeah, you are right that if a malicious agent were to be able to access the URL string of that tab then they would effectively have access to the user's details. So this might not be the best solution after all...

hi @jonluca were you able to find an alternative auth solution? in our case, we want to authenticate with a backend server using the id token so i'm also trying to make this work

Was this page helpful?
0 / 5 - 0 ratings