kulpa / google-api-python-client

Automatically exported from code.google.com/p/google-api-python-client
Other
0 stars 0 forks source link

Getting "invalid_grant" error on token refresh #90

Closed GoogleCodeExporter closed 9 years ago

GoogleCodeExporter commented 9 years ago
[Use this form for both apiclient and oauth2client issues]

What steps will reproduce the problem?
1. Authorise a django user to connect with Google Api (working fine)
2. Then leave some time for the token to expire.
3. When requesting again the same page with access to the calendar API I get 
the following error:

AccessTokenRefreshError at /googleapi
invalid_grant
Request Method: GET
Request URL:    http://localhost:8000/googleapi
Django Version: 1.3
Exception Type: AccessTokenRefreshError
Exception Value:    
invalid_grant
Exception 
Location:   /Library/Python/2.7/site-packages/google_api_python_client-1.0beta7-py
2.7.egg/oauth2client/client.py in _do_refresh_request, line 469
Python Executable:  /usr/bin/python
Python Version: 2.7.1

4. If I refresh the page a second time, then it works fine again.

The following line triggers the error:

events = service.events().list(calendarId='primary').execute()

What is the expected output? What do you see instead?

I'm expected Google to refresh the token with a new valid token the first time, 
not after reloading the page a second time.

What version of the product are you using? On what operating system?

I'm using Django 1.3.0, Python 2.7, Mac OSX Lion

Please provide any additional information below.

I have attached the full django error as a html file

Original issue reported on code.google.com by guilla...@cotidia.com on 30 Jan 2012 at 11:49

Attachments:

GoogleCodeExporter commented 9 years ago
I've been having similar problems.  But hadn't wanted to lodge a bug until I 
had investigated more thoroughly.  

In my case the credential object was still saying it was a valid credential 
even though Google had stopped accepting it as such.  My workaround has been to 
just catch the exception and redirect the user back to re-authenticate.

My hunch is that this started happening when google shortened the length of the 
token and required use of the longer lived refresh token (where you request 
offline access).  One thing I'm not clear on is whether the api will use the 
refresh token if available (It will definitely store it in the credential 
object) - using offline access doesn't avoid the problem.

Original comment by danhagga...@gmail.com on 31 Jan 2012 at 4:40

GoogleCodeExporter commented 9 years ago
These errors shouldn't occur with the defaults in the library, that is, the 
initial request should default to access_type=offline, which means the response 
should come back with a refresh_token which will be used to get access_tokens 
as needed.

Can you confirm that the initial URI that you are directed to has 
access_type=offline in the query parameters?

Original comment by jcgregorio@google.com on 31 Jan 2012 at 4:56

GoogleCodeExporter commented 9 years ago
@dan Thank you for your comment, it does make sense! Also I agree this isn't a 
bug as such but I haven't found place to discuss the google-api-python-client 
related issues.

I've thought about bypassing the error to allow re-authenticate as well but I'm 
sure there's a more elegant solution, as this would push my users to re-log in 
Google everytime if they're signed out, which could be painful!

@jc I'm away from my source right now so I will check later and see which 
access_type is setup on my calls.

Cheers
G

Original comment by guilla...@cotidia.com on 31 Jan 2012 at 9:41

GoogleCodeExporter commented 9 years ago
No - for me it doesn't include access_type=offline in the url unless I put it 
into the params when calling OAuth2WebServerFlow.

This is part of why I was hesitant about logging this bug because I looked over 
the code for OAuth2WebServerFlow - and the default param for offline access is 
right there.  So I was a bit perplexed as to what was going on.

I need to double check my claim that I was getting a accesstokenrefresherror 
even after adding the access_type=offline param.  This was a while ago now and 
I didn't have time to investigate properly.  But I don't think I've been 
getting the error after I added in "approval_prompt=force" as well...

http://googleappsdeveloper.blogspot.com.au/2011/10/upcoming-changes-to-oauth-20-
endpoint.html

...which is the advice in this post.  (and I notice it's not in the defaults).

Original comment by danhagga...@gmail.com on 31 Jan 2012 at 10:06

GoogleCodeExporter commented 9 years ago
[deleted comment]
GoogleCodeExporter commented 9 years ago
Ok I have check my code and I can see that the access_type variable is setup by 
defaults to 'offline' on line 818 in client.py:

    self.params = {
        'access_type': 'offline',
        } 

Also I have raise the URI in step1_get_authorize_url() to see if it contains 
the access_type=offline var and it does:

https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com
%2Fauth%2Fcalendar&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fgoogleapi%2Foaut
h2callback&response_type=code&client_id=xxx&access_type=offline

I'm still having the same invalid_grant error when the access_token expires. 
I'm working on localhost though, but I guess that should not make a difference.

I understand the credential value that is saved in db against a user is the 
refresh_token right? The one that should allow me to request new access tokens?

Original comment by guilla...@cotidia.com on 31 Jan 2012 at 10:52

GoogleCodeExporter commented 9 years ago
I'm still stuck, anyone can help me on this?

Original comment by guilla...@cotidia.com on 3 Feb 2012 at 1:29

GoogleCodeExporter commented 9 years ago
I am having the same issue.

Original comment by kyawt...@gmail.com on 3 Feb 2012 at 6:52

GoogleCodeExporter commented 9 years ago
Please star the issue instead of saying "me too"

Original comment by michael....@gmail.com on 3 Feb 2012 at 6:57

GoogleCodeExporter commented 9 years ago
Okay - I'm pretty sure I've figured this out.  First of all - my previous 
comments were confused.  easy_install hadn't updated my .pth file so I was 
still using the old client - which is why the access_type=offline defaults 
weren't being set properly.  So that's no longer an issue for me.

What's causing it - I think are old permissions granted by the user to the 
application when offline access wasn't granted. (When versions beta6 and below 
were being used)

Here's how to reproduce the bug EXACTLY.  Make sure you are using the beta7 
version of the client.

1) log into your google account manager:

    https://accounts.google.com/Login

2) Next to "Authorizing applications & sites" click "edit"
3) Next to "Your application name" click "revoke access" (if for some reason 
there are multiple - kill them all).
4) Log out of your google account - and log out of your application (making 
sure that no credential objects exist in storage for your user).
5) We're going to simulate what version beta6 was doing - so open client.py in 
oauth2client and comment out line 817 which reads:         "'access_type': 
'offline',"
6) Depending on what you're using - I find it easy to enter in an undeclared 
variable at line 885 in client.py so Django is forced to display the error page 
(with debug=true in the settings.py of your app).  This will make it easy to 
see if a refresh token is being returned.
7) Save it. Reload your app with the new code.
8) Send your user through the oauth process.  You'll have to sign in to google 
- and grant permission to your app.  Notice that it shouldn't ask for offline 
access.
9) oauth2client commences the second leg - sending a request to google.  If 
you're using Django and can read the error output as I suggested - then in 
variable 'd' you'll see no refresh_token.  (Otherwise go inspect your 
credential object - wherever you store it.  It shouldn't have a refresh token.  
There is nothing unusual here so far - everything is working as it should.
10) Go back into client.py and reinstate line 817 - so it will now ask for 
offline access.
11) log out of your google account.  If your app saved a credential object for 
your user - go and delete it (make sure of this every time you go through the 
process)
12) Go through the oauth2 process again.  Again a refresh_token won't be 
returned.  This is because the original permissions granted to the app are 
still in force even though you log in again and asked for offline access.  This 
is because it doesn't ask you to re-grant permission.  The lack of 
refresh_token is causing the invalid_grant error.
13) Now to do it all another time - but with one change.  Wherever in your code 
you call OAuth2WebServerFlow - include the param: approval_prompt='force'.
14) Log out of google - go through the oauth process again.  This time it will 
ask you to give permission again - but this time it will say that offline 
access is being asked for.  Give approval.  This time a refresh token will be 
returned - because in this instance you granted offline access.
15) BUT!  The original permission that google has saved for the your user 
hasn't updated.  Unless permission is granted every time - it will revert to 
failing to provide the refresh token.  To see this - remove the 
approval_prompt='force' param we just added and go through the process again.  
You'll see that even though you explicitly granted permission for offline 
access - it reverts back to the ORIGINAL permission that you gave and fails to 
return a refresh token - even though offline access was sent in the call.
16) Worse - after running one more test this bug will still happen even if you 
revoke access to the application and grant offline access in the first 
instance.  When you explicitly grant permission - it returns a refresh token.  
But when you go through the oauth process a second time - it stops sending the 
refresh token.

It may be that google is not ever updating the permissions saved for a user - 
even if you revoke access and then re grant.  When I first gave permission - 
offline access was not asked for (because it wasn't the default and the flow 
didn't need it for long lived tokens).

We need to test with a completely fresh user and have them grant offline access 
in the first instance and then see if re-authenticating (without explicitly 
granting offline access permissions) fails to deliver a refresh token.

Since it looks like not too many people are suffering from this - I'd guess 
that new users are fine.

Original comment by danhagga...@gmail.com on 4 Feb 2012 at 9:47

GoogleCodeExporter commented 9 years ago
Thanks for the detailed write up, I've forwarded this on to the auth team to 
look at.

Original comment by jcgregorio@google.com on 5 Feb 2012 at 4:54

GoogleCodeExporter commented 9 years ago
Thank you Dan for the thorough description! I'm glad the bug has been accepted 
as I'm still suffering from this, eventhough I've been using version beta7 from 
the start (i've only been using google oauth for a couple of weeks!)

I have used a completely new Google account following your instructions (but 
not using approval_prompt=false, all the settings are defaults) and so far it 
seems to stayed logged in... It is strange since I've never used the older 
package, but still the first google account i ever granted access to seems to 
have the invalid_grant error.

Original comment by guilla...@cotidia.com on 5 Feb 2012 at 10:06

GoogleCodeExporter commented 9 years ago
"""15) BUT!  The original permission that google has saved for the your user 
hasn't updated.  Unless permission is granted every time - it will revert to 
failing to provide the refresh token.  To see this - remove the 
approval_prompt='force' param we just added and go through the process again.  
You'll see that even though you explicitly granted permission for offline 
access - it reverts back to the ORIGINAL permission that you gave and fails to 
return a refresh token - even though offline access was sent in the call."""

If you want to switch from online to offline access you will need to delete the 
credentials of the user who went through the online approval process and have 
them go through the offline flow. You can detect if a user went through the 
online flow by seeing if the credentials has .refresh_token=None. 
I just checked in a change that adds a delete() method to Storages, and have 
another change pending that adds the delete() method to the Django Storage.

Original comment by jcgregorio@google.com on 6 Feb 2012 at 7:27

GoogleCodeExporter commented 9 years ago
Since i have gone through the offline process with a brand new Google account, 
everything seems to be working fine, I still get access after 48h of first 
granting.

I look forward to see the delete() method being implemented. Also, would that 
work as well as a revoking function, in case the user would like to unlink his 
google account from the oauth app?

Original comment by guilla...@cotidia.com on 6 Feb 2012 at 7:38

GoogleCodeExporter commented 9 years ago
Right now delete() just removes the credentials object from storage. To really 
unlink the google account from the app the refresh_token should be revoked:

   http://code.google.com/apis/accounts/docs/OAuth2WebServer.html#tokenrevoke

Credentials should probably have a .revoke() method that makes that revocation 
call and then calls .delete() on the underlying storage. I'll log that as a 
feature request.

Original comment by jcgregorio@google.com on 6 Feb 2012 at 7:47

GoogleCodeExporter commented 9 years ago
Feature request logged here: 
http://code.google.com/p/google-api-python-client/issues/detail?id=98

Original comment by jcgregorio@google.com on 6 Feb 2012 at 7:50

GoogleCodeExporter commented 9 years ago
OK, that's brilliant, thank you. I will implement the revoke function in my 
views for the time being.

Original comment by guilla...@cotidia.com on 6 Feb 2012 at 7:53

GoogleCodeExporter commented 9 years ago
One last follow-up on this issue. If you wanted to use access_type=online, then 
in the django_sample you could change the check from:

  storage = Storage(CredentialsModel, 'id', request.user, 'credential')
  credential = storage.get()
  if credential is None or credential.invalid == True:
    ...

to:

  storage = Storage(CredentialsModel, 'id', request.user, 'credential')
  credential = storage.get()
  if credential is None or credential.access_token_expired() == True:
    ..

which would cause the flow to go through a redirect to get a new access_token 
w/o needing the refresh_token. This will just be a few automatic redirects if 
you use approval_prompt='auto'.

Original comment by jcgregorio@google.com on 6 Feb 2012 at 8:55

GoogleCodeExporter commented 9 years ago
Code to add .delete() method to Django Storages committed in 
http://code.google.com/p/google-api-python-client/source/detail?r=40924b2a9cd2ef
1541e7f50a8acf72d9364f108a

Original comment by jcgregorio@google.com on 6 Feb 2012 at 9:58

GoogleCodeExporter commented 9 years ago
[deleted comment]
GoogleCodeExporter commented 9 years ago
Thank you Joe for adding those delete methods. I have pulled the latest branch 
and it's seems to work perfectly fine.

Though, I have finally identified the source of my issue.

The problem of invalid_grant error appeared because I had deleted my credential 
storage but not revoked the access from my Google account. So the next time I 
would authorize a user, Google would *not* return a refresh_token (even though 
I set access_type=offline).

Here's the response I get from a totally fresh (or revoked) Google account:

{"_module": "oauth2client.client", "_class": "OAuth2Credentials", 
"access_token": "xxxxxxxxxxxxxxxxxxxxxx", "token_uri": 
"https://accounts.google.com/o/oauth2/token", "invalid": false, 
"refresh_token": "xxxxxxxxxxxxxxxxxxxxxx", "user_agent": "my app", "client_id": 
"22798925268.apps.googleusercontent.com", "id_token": null, "client_secret": 
"xxx", "token_expiry": "2012-02-07T02:05:59Z"} 

Here's the response if I re-authorise a user which didn't revoked (but had its 
credential storage deleted):

{"_module": "oauth2client.client", "_class": "OAuth2Credentials", 
"access_token": "xxxxxxxxxxxxxxxxxxxxxx", "token_uri": 
"https://accounts.google.com/o/oauth2/token", "invalid": false, 
"refresh_token": null, "user_agent": "my app", "client_id": 
"22798925268.apps.googleusercontent.com", "id_token": null, "client_secret": 
"xxx", "token_expiry": "2012-02-07T02:07:52Z"}

Please note that this time the refresh token is NULL, eventhough the URI call 
to Google Oauth is strictly the same!

I would conclude that the access *must* be revoked as well when deleting the 
credentials, otherwise the next authorisation from the same user will not be 
offline.

I have tried to implement the following code in my view to revoke access 
automatically, but it doesn't work:

        try:
        storage = Storage(CredentialsModel, 'id', user, 'credential')
        credential = storage.get()
        http = httplib2.Http()
        body = {'refresh_token':credential.refresh_token}
        response, content = http.request('https://accounts.google.com/o/oauth2/revoke', 'GET', body=urllib.urlencode(body))
        storage.delete()
    except CredentialsModel.DoesNotExist:
        pass

So at the moment, I manually revoke from my Google account page.

Original comment by guilla...@cotidia.com on 7 Feb 2012 at 1:22

GoogleCodeExporter commented 9 years ago
"""
Please note that this time the refresh token is NULL, eventhough the URI call 
to Google Oauth is strictly the same!
"""

Yes, but the grant was initially given as access_type=online, so all subsequent 
requests will just get you a fresh
access token and not a refresh token, that is, it is operating as designed.

You wrote:

   body = {'refresh_token':credential.refresh_token}
   response, content = http.request('https://accounts.google.com/o/oauth2/revoke', 'GET', body=urllib.urlencode(body))

That should actually be:

   query = urllib.urlencode({'token': str(credential.refresh_token}))
   http.request('https://accounts.google.com/o/oauth2/revoke?' + query), 'GET')

Original comment by jcgregorio@google.com on 7 Feb 2012 at 1:59

GoogleCodeExporter commented 9 years ago
Thank you Joe for you help, now my integration works perfectly fine.

I will follow Issue 98 so I can implement the revoke method as soon as it gets 
added.

Btw, i have a corrected a few syntax error in the revoke call just in case 
someone else would like to copy it and use it:

query = urllib.urlencode({'token': str(credential.refresh_token)})
http.request('https://accounts.google.com/o/oauth2/revoke?' + query, 'GET')

Original comment by guilla...@cotidia.com on 7 Feb 2012 at 11:55

GoogleCodeExporter commented 9 years ago

Original comment by jcgregorio@google.com on 23 Feb 2012 at 6:32

GoogleCodeExporter commented 9 years ago
There is an easier way from a post up above.  You don't need to revoke access, 
you just force the approval prompt and it will send a refresh token every time. 
 Then you don't need to worry about it ever again.  Read this post from the 
programmer at google that wrote it.

"approval_prompt=force" is what you want to add

http://googleappsdeveloper.blogspot.com.au/2011/10/upcoming-changes-to-oauth-20-
endpoint.html

Original comment by rboy...@alvaradoisd.net on 18 Mar 2014 at 10:39