I have a scenario where I am obtaining access/refresh tokens via oAuth consent. When the access token expires, the refreshed token returned after refresh is losing scopes previously acquired.
Summary of the interesting parts of the code look like this:
// I'm using google-auth-library
import { OAuth2Client } from "google-auth-library";
...
// I create an OAuth2Client like this. Let's assume the parameters are correct (they are)
new OAuth2Client(
clientId,
clientSecret,
redirectUri
);
When obtaining consent, I generate a URL like this (this.newOAuth2Client
just creates the client as described above):
// Generate a url that asks permissions for the Drive activity scope
const authorizationUrl = this.newOAuth2Client(redirectUri).generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: 'offline',
prompt : 'select_account',
// Pass in the scopes array.
scope: scopes,
// Enable incremental authorization. Recommended as a best practice.
include_granted_scopes: true,
// The value to be passed to the redirect URL.
state: state
});
When it comes time to refresh the token, it looks like this:
const client = this.newOAuth2Client(redirectUri);
client.setCredentials({ refresh_token: refresh_token });
return await client.getAccessToken();
The token returned produces a 401 error against the (Drive) API I am accessing, for requests that worked just fine prior to the refresh.
This is how I’m pulling the updated tokens from the refresh:
/**
* The client.getAccessToken() returns something called a GetAccessTokenResponse,
* which seems to be undocumented (?). Hunting and pecking...
*/
const result = client.getAccessToken();
const response = result.res;
if(response.status == 200) {
const credentials = response.data;
const accessToken = credentials.access_token; // <== This seems broken
const refreshToken = credentials.refresh_token;
}
The only other piece of relevant data might be that these lost scopes were originally obtained after initial consent. In my particular case, I am first requesting these scopes:
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile'
Later, I add the following scopes:
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.appfolder',
'https://www.googleapis.com/auth/drive.file'
After the refresh, the scopes seem to revert to only the initial set. After this subsequent grant, there is an update to the access_token
(as expected) and if there is a refresh_token
present I will persist it, but generally a refresh_token
is not provided on this incremental grant.
Is it expected that refreshing an access token would wipe out scopes, or is there something wonky with my approach? (or perhaps my problem is elsewhere entirely?)
Edit:
Reviewing the documentation for incremental scope grants:
When you use the refresh token for the combined authorization to obtain an access token, the access token represents the combined authorization and can be used for any of the scope values included in the response.
This is not happening for me. It seems unlikely that Google’s oauth mechanism is broken, so my assumption is that I’m doing something wrong, but just can’t see what.
2
Answers
OK.. I think I figured it out.
This section of the code posted
Is wrong. Specifically the call to
client.getAccessToken()
. It should be thisAlthough there is a conspicuous lack of documentation for these classes, after leafing through the source code (here, and here), I quickly realized that the underlying API is actually very simple, so was able to re-create the scenario using
curl
. For anyone interested...Now I'm sure when I tried calling this API endpoint the first time, I saw the same problem, but I may have had a momentary loss of cognitive function because every subsequent time it seemed to work.
The
grant_type
in thecurl
command is not needed in the client library as it's actually hard coded in the library itself.(Sidebar rant: while it's great that these client libs are open source, combing through source code is somewhat less efficient than having actual documentation that's more than auto generated docs from source. I've pretty much abandoned using Google's nodejs client libraries, which seem like an overly complex swiss-army knife, in favor of directly calling to what are actually a very simple, straightforward server API surfaces for most of Google's APIs. End rant)
To ensure that the access token covers all the scopes the user has previously granted, you need to set the include_granted_scopes parameter to true when generating the authorization URL using the method
generateAuthUrl()
.According to Google doc: “Enables applications to use incremental authorization to request access to additional scopes in context. If you set this parameter’s value to true and the authorization request is granted, then the new access token will also cover any scopes to which the user previously granted the application access.”
Source: https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest/google-auth-library/generateauthurlopts