Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

from_authorized_user_file always returns invalid credentials #501

Closed
busunkim96 opened this issue May 6, 2020 · 14 comments
Closed

from_authorized_user_file always returns invalid credentials #501

busunkim96 opened this issue May 6, 2020 · 14 comments
Assignees
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@busunkim96
Copy link
Contributor

busunkim96 commented May 6, 2020

from_user_info currently always sets the token to None. So a credential created from a JSON is always invalid and must be refreshed.

>>> from google.oauth2 import credentials
>>> TOKENS = 'tokens.json' # OAuth2 token storage
>>> creds = credentials.Credentials.from_authorized_user_file(TOKENS)
>>> creds.valid
False
>>> creds.expiry
>>> creds.expired
False

return cls(
None, # No access token, must be refreshed.
refresh_token=info["refresh_token"],
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,
scopes=scopes,
client_id=info["client_id"],
client_secret=info["client_secret"],
quota_project_id=info.get(
"quota_project_id"
), # quota project may not exist

If a token is available, from_user_info should use it. info.get('token', None)

@busunkim96 busunkim96 added priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels May 6, 2020
@busunkim96 busunkim96 changed the title from_authorized_user_file always returns invalid/expired credentials from_authorized_user_file always returns invalid credentials May 6, 2020
@alvyjudy
Copy link
Contributor

alvyjudy commented May 6, 2020

I wonder if this was intentional because the access token is usually set to expire within a short period of time? It is likely expired in this context if it were spun up again from a JSON file.

@busunkim96
Copy link
Contributor Author

@alvyjudy I believe the token is valid for ~1 hour. If the token is expired the refresh logic should kick in, so I don't it's harmful to use token from the JSON.

@property
def valid(self):
"""Checks the validity of the credentials.
This is True if the credentials have a :attr:`token` and the token
is not :attr:`expired`.
"""
return self.token is not None and not self.expired

def before_request(self, request, method, url, headers):
"""Performs credential-specific before request logic.
Refreshes the credentials if necessary, then calls :meth:`apply` to
apply the token to the authentication header.
Args:
request (google.auth.transport.Request): The object used to make
HTTP requests.
method (str): The request's HTTP method or the RPC method being
invoked.
url (str): The request's URI or the RPC service's URI.
headers (Mapping): The request's headers.
"""
# pylint: disable=unused-argument
# (Subclasses may use these arguments to ascertain information about
# the http request.)
if not self.valid:
self.refresh(request)
self.apply(headers)

@alvyjudy
Copy link
Contributor

alvyjudy commented May 7, 2020

@busunkim96 I feel the way self.expired and self.valid work wouldn't allow us to just pass in the token to from_authorized_user_info

@property
def expired(self):
"""Checks if the credentials are expired.
Note that credentials can be invalid but not expired because
Credentials with :attr:`expiry` set to None is considered to never
expire.
"""
if not self.expiry:
return False
# Remove 5 minutes from expiry to err on the side of reporting
# expiration early so that we avoid the 401-refresh-retry loop.
skewed_expiry = self.expiry - _helpers.CLOCK_SKEW
return _helpers.utcnow() >= skewed_expiry

Since self.expiry won't be set when a credential is freshly constructed, self.expired will always return False.

This will cause self.valid to always return True if token is passed in

@property
def valid(self):
"""Checks the validity of the credentials.
This is True if the credentials have a :attr:`token` and the token
is not :attr:`expired`.
"""
return self.token is not None and not self.expired

and self.refresh won't be called even if the token did expire. Proceeding with the code will result in a invalid token response when the token is actually valid but expired.

Please correct me if I had made a mistake interpreting the code (I'm quite new here:)

@alvyjudy
Copy link
Contributor

alvyjudy commented May 7, 2020

A simpler fix would be just to add a line of code to refresh the credential before returning it. I can open a PR if that's desired

@wescpy
Copy link
Contributor

wescpy commented May 8, 2020

I feel that will change our "contract" with the developer. When there's a valid access token stored, users will expect no refresh until after the 3600s have passed for it. It'd be better if we take that access token verbatim and ensure all the flags are set appropriately for however much longer that token's TTL is good for.

@alvyjudy
Copy link
Contributor

alvyjudy commented May 9, 2020

@wescpy I agree with what you said. It's just that currently to_json method would strip away the expiry information making it hard to keep track of the TTL. Would it be a better idea to pickle that information in to_json and parse it too when picking up the token in from_authorized_user_info?

@wescpy
Copy link
Contributor

wescpy commented May 11, 2020

That sounds much better. Was there a particular reason why it (expiry) wasn't included when 1st implemented (to better understand the design decision at the time... wasn't a tz issue was it)?

@busunkim96
Copy link
Contributor Author

@wescpy I don't think so. I believe it was modeled closely after oauth2client, so we might have to take another look there to see if there was a reason for exclusion.

@wescpy
Copy link
Contributor

wescpy commented May 12, 2020

SG. (Of course) oauth2client didn't have any issues w/being able to process both active access tokens or run the refresh process if needed. Since it managed the tokens and everyone used that code, it was probably fairly "hardened" (hence why I think at some point in the future, we should also add that feature to this library or google_auth_oauthlib [as well as the client libs for the other languages]... it's much better UX if developers didn't have to worry about token timeout issues which are orthogonal to the solutions they're building).

@dd-ssc
Copy link

dd-ssc commented Jun 14, 2020

I stumbled across this while working my way from the GMail Python Quickstart towards a production app. That sample app saves/loads the token to/from a local token.pickle binary Python pickle file, including the access token.

In our projects, we store all secrets in a HashiCorp Vault instance, so a non-binary, text representation is needed for persistence. As a first step towards that, I replaced the local pickle file by a local json file.

When I run the app subsquently during development, the access token is still perfectly valid; there is no reason to refresh. Still, the credentials returned by from_authorized_user_info() are invalid.
As a result, it isn't even attempted to refresh the access token by means of the refresh token - the entire user auth process is started every time: Please visit this URL to authorize this application in Terminal, Browser window opens, user logs into their GMail / GSuite account, etc.

Also, why would from_authorized_user_file (or from_authorized_user_info it uses internally) make any decision about the token validity ? IMO, the job of persistence in general is to freeze information into storage and thaw it back into the app as needed - and the info that comes back out should obviously be equal to what was put in. The access token may have expired since it was persisted, but that's not for from_authorized_user_file to decide, is it ?

I think I can work around this for the time being by just re-adding the access token to the credentials, but from my point of view, this just feels like a bug.

@dd-ssc
Copy link

dd-ssc commented Jun 14, 2020

Update: The workaround for this issue is trivial, posting it here in case anyone else runs into it:

import json
from google.oauth2.credentials import Credentials

def load_user_secrets_from_local(user_secrets_file):
    if exists(user_secrets_file):
        with open(user_secrets_file, 'r') as stream:
            creds_json = json.load(stream)
        creds = Credentials.from_authorized_user_info(creds_json)
        # workaround for
        # https://github.com/googleapis/google-auth-library-python/issues/501
        creds.token = creds_json['token']
        return creds
    return None

wescpy added a commit to wescpy/google-auth-library-python that referenced this issue Aug 10, 2020
- The access token is always set to `None`, so the fix involves using (the access) `token` from the saved JSON credentials file.
- For refresh needs, `expiry` also needs to be saved via `to_json()`.
    - DUMP: As `expiry` is a `datetime.datetime` object, serialize to `datetime.isoformat()` in the same [`oauth2client` format](https://github.com/googleapis/oauth2client/blob/master/oauth2client/client.py#L55) for consistency.
    - LOAD: Add code to restore `expiry` back to `datetime.datetime` object when imported.
    - LOAD: If `expiry` was unsaved, automatically set it as expired so refresh takes place.
- Minor `scopes` updates
    - DUMP: Add property for `scopes` so `to_json()` can grab it
    - LOAD: `scopes` may be saved as a string instead of a JSON array (Python list), so ensure it is Sequence[str] when imported.
@wescpy
Copy link
Contributor

wescpy commented Aug 10, 2020

A workaround is good but not a permanent fix for this bug, so I spent the day working on it and submitted for review as </pull/589>. (A couple of supporting tweaks also needed to be made.) Thanks to @dd-ssc for the inspiration!

busunkim96 pushed a commit that referenced this issue Sep 17, 2020
* This patch for </issues/501> includes the following fixes:

- The access token is always set to `None`, so the fix involves using (the access) `token` from the saved JSON credentials file.
- For refresh needs, `expiry` also needs to be saved via `to_json()`.
    - DUMP: As `expiry` is a `datetime.datetime` object, serialize to `datetime.isoformat()` in the same [`oauth2client` format](https://github.com/googleapis/oauth2client/blob/master/oauth2client/client.py#L55) for consistency.
    - LOAD: Add code to restore `expiry` back to `datetime.datetime` object when imported.
    - LOAD: If `expiry` was unsaved, automatically set it as expired so refresh takes place.
- Minor `scopes` updates
    - DUMP: Add property for `scopes` so `to_json()` can grab it
    - LOAD: `scopes` may be saved as a string instead of a JSON array (Python list), so ensure it is Sequence[str] when imported.
@wescpy
Copy link
Contributor

wescpy commented Sep 23, 2020

While the PR has been merged, this isn't officially "fixed" until the next release. However, those who are being bitten by this can at least grab the latest google/oauth2/credentials.py and replace their faulty one.

@busunkim96
Copy link
Contributor Author

Closed by #589

busunkim96 added a commit that referenced this issue Oct 22, 2020
* refactor: split 'with_quota_project' into separate base class (#561)

Co-authored-by: Tres Seaver <tseaver@palladion.com>

* fix: dummy commit to trigger a auto release (#597)

* chore: release 1.21.1 (#599)

* chore: updated CHANGELOG.md [ci skip]

* chore: updated setup.cfg [ci skip]

* chore: updated setup.py

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>

* fix: migrate signBlob to iamcredentials.googleapis.com (#600)

Migrate signBlob from iam.googleapis.com to iamcredentials.googleapis.com.

This API is deprecated and will be shutdown in one year.

This is used google.auth.iam.Signer.
Added a system_test to sanity check the implementation.

* chore: release 1.21.2 (#601)

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>

* fix: fix expiry for `to_json()` (#589)

* This patch for </issues/501> includes the following fixes:

- The access token is always set to `None`, so the fix involves using (the access) `token` from the saved JSON credentials file.
- For refresh needs, `expiry` also needs to be saved via `to_json()`.
    - DUMP: As `expiry` is a `datetime.datetime` object, serialize to `datetime.isoformat()` in the same [`oauth2client` format](https://github.com/googleapis/oauth2client/blob/master/oauth2client/client.py#L55) for consistency.
    - LOAD: Add code to restore `expiry` back to `datetime.datetime` object when imported.
    - LOAD: If `expiry` was unsaved, automatically set it as expired so refresh takes place.
- Minor `scopes` updates
    - DUMP: Add property for `scopes` so `to_json()` can grab it
    - LOAD: `scopes` may be saved as a string instead of a JSON array (Python list), so ensure it is Sequence[str] when imported.

* chore: add default CODEOWNERS (#609)

* chore: release 1.21.3 (#607)

* feat: add asyncio based auth flow (#612)

* feat: asyncio http request logic and asynchronous credentials logic  (#572)

Co-authored-by: Anirudh Baddepudi <43104821+anibadde@users.noreply.github.com>

* chore: release 1.22.0 (#615)

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>

* fix: move aiohttp to extra as it is currently internal surface (#619)

Fix #618. Removes aiohttp from required dependencies to lessen dependency tree for google-auth.

This will need to be looked at again as more folks use aiohttp and once the surfaces goes to public visibility.

* chore: release 1.22.1 (#620)

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>

* fix: remove checks for ancient versions of Cryptography (#596)

Refs #595 (comment) 

I see no point in checking whether someone is running a version of https://github.com/pyca/cryptography/ from 2014 that doesn't even compile against modern versions of OpenSSL anymore.

* chore: sync to master

Syncs to master.
Fixes broken unit tests in Python 3.6 and 3.7.
Aligns test_identity_pool.py with test_aws.py.

Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com>
Co-authored-by: Tres Seaver <tseaver@palladion.com>
Co-authored-by: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com>
Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: wesley chun <wescpy@gmail.com>
Co-authored-by: Christopher Wilcox <crwilcox@google.com>
Co-authored-by: Anirudh Baddepudi <43104821+anibadde@users.noreply.github.com>
Co-authored-by: Aarni Koskela <akx@iki.fi>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

4 participants