Closed GoogleCodeExporter closed 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
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
@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
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
[deleted comment]
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
I'm still stuck, anyone can help me on this?
Original comment by guilla...@cotidia.com
on 3 Feb 2012 at 1:29
I am having the same issue.
Original comment by kyawt...@gmail.com
on 3 Feb 2012 at 6:52
Please star the issue instead of saying "me too"
Original comment by michael....@gmail.com
on 3 Feb 2012 at 6:57
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
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
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
"""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
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
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
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
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
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
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
[deleted comment]
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
"""
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
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
Original comment by jcgregorio@google.com
on 23 Feb 2012 at 6:32
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
Original issue reported on code.google.com by
guilla...@cotidia.com
on 30 Jan 2012 at 11:49Attachments: