googleapis / google-auth-library-python-oauthlib

Apache License 2.0
186 stars 82 forks source link

Refresh_token not regenerated with current authorization_url defaults. #348

Closed octaflop closed 4 months ago

octaflop commented 6 months ago

Thanks for stopping by to let us know something could be better!

PLEASE READ: If you have a support contract with Google, please create an issue in the support console instead of filing on GitHub. This will ensure a timely response.

Please run down the following list and make sure you've tried the usual "quick fixes":

If you are still having issues, please be sure to include as much information as possible:

Environment details

Steps to reproduce

  1. Auth and save a token.json
  2. Delete the token.json
  3. Try to use the token.json or note it doesn't have a refresh_token.
  4. The token.json will fail to work

Code example

def create_service() -> discovery:
    token_path = Path(__file__).parent / 'token.json'
    credentials_path = Path(__file__).parent / 'client_secrets.json'
    creds = None
    if token_path.exists():
        try:
            creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
        except ValueError:
            creds = None
    # 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:
            flow = InstalledAppFlow.from_client_secrets_file(
                str(credentials_path), SCOPES)
            try:
                creds = flow.run_local_server(port=8080, open_browser=False)

            except MismatchingStateError:
                print("State mismatch error during OAuth process. Trying again...")
                creds = flow.run_local_server(port=8080, open_browser=False)

        # Save the credentials for the next run
        with open(token_path, 'w') as token:
            token.write(creds.to_json())

    service = build('drive', 'v3', credentials=creds)
    return service

Stack trace

NA -- ValueError

Making sure to follow these steps will guarantee the quickest resolution possible.

Thanks!

Proposed fix (PR incoming)

add prompt=consent to the authorization_url as a default. As a current workaround I am finding this to work:

url_args = {
    'access_type': 'offline',
    'prompt': 'consent'
}
flow = InstalledAppFlow.from_client_secrets_file(
    str(credentials_path), SCOPES)
try:
    creds = flow.run_local_server(port=8080, open_browser=False, **url_args)

except MismatchingStateError:
    print("State mismatch error during OAuth process. Trying again...")
    creds = flow.run_local_server(port=8080, open_browser=False, **url_args)
arithmetic1728 commented 5 months ago

I am inclined not to add prompt=consent because this changes the behavior of flow.run_local_server, it will make flow.run_local_server always show the consent page. https://developers.google.com/identity/openid-connect/openid-connect#authenticationuriparameters

octaflop commented 5 months ago

Great point. I'm not sure how best to approach parameterizing or retrying in this edge case. Any thoughts would be helpful.

arithmetic1728 commented 5 months ago
if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
        try:
            creds.refresh(Request())
            # Save the credentials for the next run
            with open(token_path, 'w') as token:
                token.write(creds.to_json())
        except google.auth.exceptions.RefreshError:
            # fall back to run_local_server
            pass

    flow = InstalledAppFlow.from_client_secrets_file(
            str(credentials_path), SCOPES)
    url_args = {
        'access_type': 'offline',
        'prompt': 'consent'
    }
    try:
        creds = flow.run_local_server(port=8080, open_browser=False, **url_args)
    except MismatchingStateError:
        print("State mismatch error during OAuth process. Trying again...")
        creds = flow.run_local_server(port=8080, open_browser=False, **url_args)

    # Save the credentials for the next run
    with open(token_path, 'w') as token:
        token.write(creds.to_json())

you may try this approach. If there is refresh token, just refresh an access token; if we cannot refresh or refresh fails due to expired refresh token, fall back to run_local_server with the url_args, this way you will get a new refresh_token.

arithmetic1728 commented 4 months ago

Please reopen the issue if you have any questions. Thanks!