Reputation: 81454
Problem: Missing OAuth 2 Refresh Token.
The problem is that the localhost version receives a Refresh Token
as part of the granted token but the same code running in GCE does not.
Details:
I have written a Python Flask application that implements Google OAuth 2.0. This web application runs in the cloud with a verified domain name, valid SSL certificate and HTTPS endpoint. This web application unmodified also runs as localhost
. The differences between the runtime is that the localhost version does not use TLS. There are no other differences in the code flow.
Other than the Refresh Token
is missing and I cannot automatically renew a token
, everything works perfectly.
I have researched this issue extensively. API problems such as access_type=offline
etc are correctly implemented otherwise I would not get a Refresh Token
in the localhost
version.
I am using the requests_oauthlib
python library.
gcp = OAuth2Session(
app.config['gcp_client_id'],
scope=scope,
redirect_uri=redirect_uri)
# print('Requesting authorization url:', authorization_base_url)
authorization_url, state = gcp.authorization_url(
authorization_base_url,
access_type="offline",
prompt="select_account",
include_granted_scopes='true')
session['oauth_state'] = state
return redirect(authorization_url)
# Next section of code after the browser approves the request
token = gcp.fetch_token(
token_url,
client_secret=app.config['gcp_client_secret'],
authorization_response=request.url)
The token has refresh_token
when running in localhost
but not when running with in the cloud.
This Google document discusses refresh tokens, which indicates that this is supported for web applications.
Refreshing an access token (offline access)
[Update 11/18/2018]
I found this bug report which gave me a hint to change my code from this:
authorization_url, state = gcp.authorization_url(
authorization_base_url,
access_type="offline",
prompt="select_account",
include_granted_scopes='true')
to this:
authorization_url, state = gcp.authorization_url(
authorization_base_url,
access_type="offline",
prompt="consent",
include_granted_scopes='true')
Now I am receiving the Refresh Token in the public server version and the localhost version.
Next I searched for documentation on the prompt
option and found this:
prompt (Optional)
A space-delimited list of string values that specifies whether the authorization server prompts the user for reauthentication and consent. The possible values are:
none The authorization server does not display any authentication or user consent screens; it will return an error if the user is not already authenticated and has not pre-configured consent for the requested scopes. You can use none to check for existing authentication and/or consent.
consent The authorization server prompts the user for consent before returning information to the client.
select_account The authorization server prompts the user to select a user account. This allows a user who has multiple accounts at the authorization server to select amongst the multiple accounts that they may have current sessions for.
If no value is specified and the user has not previously authorized access, then the user is shown a consent screen.
I think the Google documentation should be updated. On the same page, the following text appears:
access_type (Optional)
The allowed values are offline and online. The effect is documented in Offline Access; if an access token is being requested, the client does not receive a refresh token unless offline is specified.
That statement caused me a lot of confusion trying to debug why I could not obtain a Refresh Token for the public server version but I could for the localhost version.
Upvotes: 56
Views: 4855
Reputation: 41
It looks like the issue stems from the way Google handles refresh token generation based on the prompt parameter. By default, if the user has already granted access, Google may not prompt for consent again, which can lead to the refresh token not being issued in subsequent requests.
Solution: The key change is modifying your prompt parameter from select_account to consent in your authorization request:
authorization_url, state = gcp.authorization_url(
authorization_base_url,
access_type="offline",
prompt="consent", # Force consent to get a refresh token
include_granted_scopes='true'
)
Why Does This Work?
-
1 . Google's Behavior with Refresh Tokens:
Google only issues a refresh token the first time a user consents to an application, or if prompt="consent" is explicitly specified.
If the user has already granted access in the past, and you don't specify prompt="consent", Google may not return a new refresh token.
2 . Different Behavior Between Localhost and GCE:
When testing locally, the authorization flow might be treated as a "new" session, leading Google to return a refresh token.
On GCE, the session might already exist, and without prompt="consent", Google assumes there's no need to issue a new refresh token.
Next Steps:
If you're still experiencing issues, try revoking access for your app via Google’s Account Permissions and re-authenticate with prompt="consent"
Ensure that access_type="offline" is always included in the request.
Hope this helps! Let me know if you need more details. good luck !
Upvotes: 0