John Hanley
John Hanley

Reputation: 81454

Google OAuth 2 Refresh Token is Missing for Web App but Present for localhost

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:

OpenID Conect prompt

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

Answers (1)

KasraFnp
KasraFnp

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

Related Questions