Google OAuth 2.0 - Incremental authorization with offline access
Asked Answered
S

2

1

I'm trying to implement incremental authorization with Google Oauth2 in my application.

At first, when users sign in, the application is requesting some scopes and offline access. I'm generating and storing the refreshToken in my database so I can refresh access tokens to make API calls when needed.

Now I'm going to implement a new functionality that will request a new scope, but I would like to ask for this scope only if users try to use this new functionality.

The problem I'm facing is that, when the application promts users to consent for new scope, Google is requesting access for all the previously accepted scopes.

I have tried without offline access (using 'grant' method) and it seems to be working (as the description says 'Request additional scopes to the user.'). Doing that, Google is only requesting new scope and the access token generated seems to work fine. But if I try with offline access (using 'grantOfflineAccess' method), Google asks for all the scopes. If I accept all the scopes again, it returns an authorization code and I can generate the refreshToken again, but I would like to do that only accepting the new scope.

I was wondering maybe it's not possible to do it when requesting offline access, as you need to generate a new refreshToken with all the scopes, but I couldn't find any information about this.

Am I doing anything wrong or is it not possible to achieve this with offline access?

UPDATE: Also generating the URL (instead of using the JS library) as explained here, Google asks for all the scopes. This would be an example of the generated URL:

https://accounts.google.com/o/oauth2/v2/auth?
  scope=my_new_scope&
  redirect_uri=https://myapp.example.com/callback&
  response_type=code&
  client_id=my_client_id&
  prompt=consent&
  include_granted_scopes=true

UPDATE2: I've found a reported issue here, where someone commented that it's not possible to do it for installed applications. But there is a comment explaining how to do it with web server flow, these are the steps using OAuth2 playground (https://developers.google.com/oauthplayground):

1) Select the Google Calendar scope.

2) Authorize access

3) Exchange code for token.

4) Make request to the token info endpoint: https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=...

5) Verify that the token only contains the Google Calendar scope.

6) De-select the Google Calendar scope and select the Google Drive scope.

7) Edit the URL in the authorization window and append &include_granted_scopes=true.

8) Authorize the Drive scope.

9) Exchange the code for a token.

10) Make request to the token info endpoint.

11) Verify that it contains both the Calendar and Drive scopes.

Doing the same I get Calendar scope in the first verification and Drive scope in the second one. Even using the refresh token I cannot get an access token with both scopes. After that, and this is pretty strange, I've checked the applications with access to my account and OAuth2 playground has both scopes:

enter image description here

Thank you in advance!

Shluh answered 24/7, 2018 at 9:24 Comment(0)
S
5

I've finally accomplished to implement incremental authorization using JS library. Using grantOfflineAccess function you can send the new scopes you want to request. By default include_granted_scopes is true when you use gapi.auth2.authorize.

So you just need to send the new scopes, for example:

GoogleUser.grantOfflineAccess({scope: 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/calendar'}

Then you will receive an authorization code to exchange for a refreshToken. You can check everything works by generating an access_token with the new refreshToken and calling the token info endpoint: https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=...

I'm sure this was something I've tried at first, so I guess I was doing something wrong.

Anyways, I have decided to not use incremental authorization due to an issue with the library. Basically the problem is that, for a better user experience and to avoid problems, you should be able to use prompt: 'consent' so the user doesn't need to select an account to accept new scopes. But this is not possible and you could have problems if users select different accounts to accept different scopes from your application.

Shluh answered 2/8, 2018 at 17:38 Comment(0)
G
1

Thanks, I had the same problem! Your answer helped me to figure it out. Here is the final workflow that we need to complete with two libraries. Hopefully, the code snippets might be helpful.

  1. Authenticate with Google Sign-In (google-auth-library) to get a refresh token.
  2. Implement incremental authorization: authorize your application to use their information to integrate it with a new Google API. For instance, Google Calendar (google-auth-library and googleapis)

Authenticate with Google Sign-In to get a refresh token

Front-end:

$('#signinButton').click(function() {
   auth2.grantOfflineAccess().then(signInCallback);
 });

Back-end:

const { OAuth2Client } = require('google-auth-library');

/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the refresh token.
*/
function getRefreshToken(code, scope) {
  return new Promise((resolve, reject) => {
    // Create an oAuth client to authorize the API call. Secrets should be 
    // downloaded from the Google Developers Console.
    const oAuth2Client = new OAuth2Client(
      YOUR_CLIENT_ID,
      YOUR_CLIENT_SECRET,
      YOUR_REDIRECT_URL
    );

    // Generate the url that will be used for the consent dialog.
    await oAuth2Client.generateAuthUrl({
      access_type: 'offline',
      scope,
    });
    
    // Verify the integrity of the idToken through the authentication 
    // code and use the user information contained in the token
    const { tokens } = await client.getToken(code);
    const ticket = await client.verifyIdToken({
      idToken: tokens.id_token!,
      audience: keys.web.client_secret,
    });
    idInfo = ticket.getPayload();
    return tokens.refresh_token;
  })
}

With this refresh token (you should persist it), you can create a Google API's client with the googleapis library anytime.

Implement incremental authorization

Front-end:

const googleOauth = gapi.auth2.getAuthInstance();
const newScope = "https://www.googleapis.com/auth/calendar"

googleOauth = auth2.currentUser.get();
googleOauth.grantOfflineAccess({ scope: newScope }).then(
    function(success){
      console.log(JSON.stringify({ message: "success", value: success }));
    },
    function(fail){
      alert(JSON.stringify({message: "fail", value: fail}));
    });

Back-end:

const { google } = require('googleapis'); 

// Create an oAuth client to authorize the API call. Secrets should be 
// downloaded from the Google Developers Console.
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

client.setCredentials({ refresh_token: refreshToken });

That's it! You can use this client to integrate any Google API with your application. For a detailed workflow with a Node.js back-end, you might find my gist helpful.

Gerius answered 20/6, 2021 at 19:18 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.