googleads / googleads-python-lib

The Python client library for Google's Ads APIs
Apache License 2.0
682 stars 975 forks source link

Why do i not get a new refresh token returned with every authorization request? #514

Closed LindaLawton closed 1 year ago

LindaLawton commented 1 year ago

The following example lists a users messages from the Gmail api using web flow.

My question is. Why does the authorization only return a refresh token after the first authorization request. I have added offline access, the first time the user authorizes this code a refresh token is returned. It is not returned again, the only way to force it to return again is to add prompt= "consent".

I have had to add a check in the oauth2callback method in order to check that there is a refresh token and to not save my credentials if the refresh token does not exist, as it was over writing it every time the access token was refreshed.

if not os.path.exists('token.json') or credentials.refresh_token:

Is there something that I need to set on the client to ensure that it always returns a new refresh token when the access token is refreshed. I thought this was a standard feature in Google authorization server it i always get a new refresh token in other languages. Or is the issue that this is a web client?

code

import os

import flask
from flask import Flask,redirect,render_template,url_for, request
app = Flask(__name__, template_folder='templates')
import google.auth.exceptions
import google_auth_oauthlib.flow
import ssl
context = ssl.SSLContext()
context.load_cert_chain('C:\Development\FreeLance\GoogleSamples\Python\cert.pem', 'C:\Development\FreeLance\GoogleSamples\Python\key.pem')
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.errors import HttpError
from googleapiclient.discovery import build
import google_auth_oauthlib.flow

SCOPES = ['https://mail.google.com/']

REDIRECT_URI = 'https://127.0.0.1:5000/oauth2callback'

CREDENTIALS = 'C:\Development\FreeLance\GoogleSamples\Credentials\CredWebEverything.json'

def get_flow():

    # Initialize the flow using the client ID and secret downloaded earlier.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CREDENTIALS,
        scopes= SCOPES,
    )
    # Indicate where the API server will redirect the user after the user completes
    # the authorization flow. The redirect URI is required.
    flow.redirect_uri = REDIRECT_URI

    return flow

def redirect_user():

    flow = get_flow()
    # Generate URL for request to Google's OAuth 2.0 server.
    # Use kwargs to set optional request parameters.
    authorization_url, state = flow.authorization_url(
        # Enable offline access so that you can refresh an access token without
        # re-prompting the user for permission. Recommended for web server apps.
        access_type='offline',
        # Enable incremental authorization. Recommended as a best practice.
        include_granted_scopes='false',
        # Forces a new refresh token when we authorize the application a second time.
        #prompt= "consent"
        )

    return authorization_url, state

@app.route('/login')
def login():
    authorization_url, state = redirect_user()
    return flask.redirect(authorization_url)

@app.route('/listmessages')
def gmail_list_messages():
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        try:
            creds = Credentials.from_authorized_user_file('token.json', SCOPES)
            print(f'Credentials exist refreshing.')
            creds.refresh(Request())
        except google.auth.exceptions.RefreshError as error:
            # if refresh token fails, reset creds to none.
            creds = None
            print(f'An error occurred: {error}')
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            # If it's not logged in then it's going to force it to.
            authorization_url, state = redirect_user()
            print(f'Credentials do not exist requesting authorization.')
            return flask.redirect(authorization_url)
    try:
        service = build('gmail', 'v1', credentials=creds)

        # Call the Gmail v1 API
        results = service.users().messages().list(
            userId='me').execute()
        messages = results.get('messages', [])
    except HttpError as error:
        # TODO(developer) - Handle errors from gmail API.
        print(f'An error occurred: {error}')

    return render_template("mail.html", data=messages)

@app.route('/')
def index():
    return render_template('index.html', title="Home Page")

@app.route('/oauth2callback')
def oauth2callback():
    flow = get_flow()

    auth_code = request.args['code']
    flow.fetch_token(code=auth_code)
    credentials = flow.credentials

    # saving the credentials for later. Note: A refresh does not return a new refresh token.
    if not os.path.exists('token.json') or credentials.refresh_token:
        print(f'Storing credentials: {credentials.to_json()}')
        with open('token.json', 'w') as token:
            token.write(credentials.to_json())

    return redirect("/listmessages")

if __name__ == '__main__':
    # Bind to PORT if defined, otherwise default to 5000.
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port, ssl_context=context)

Cross posted Why do i not get a new refresh token returned with every authorization request?

msaniscalchi commented 1 year ago

Hello, this issue is not relevant to these client libraries, so I'll be closing it out.

As a brief response to your question though, the refresh token is intended to be used to refresh your access tokens indefinitely, and there are in fact a finite number that your account may have active at any given time. In that sense, stepping through the OAuth 2.0 process to generate new refresh tokens frequently is essentially the opposite of how they are intended to be used.

If other libraries / languages are producing a new refresh token every time you refresh the access token, I would suggest that they are probably (incorrectly) stepping through the authorization step rather than simply refreshing the access token as intended.