18

I am trying to write a Django app which creates events in a specific Google calendar. So far I have been successful. There is only a little problem:

I don't know how to obtain a refresh token with the google python client.

The result is that after my token expires the app does not work and I have to create a new token. If I understand the documentation correct that's where the refresh token comes in.

Access tokens have a limited lifetime and, in some cases, an application needs access to a Google API beyond the lifetime of a single access token. When this is the case, your application can obtain what is called a refresh token. A refresh token allows your application to obtain new access tokens.

Google Documentation (see "Basic Steps", Section 4)

My code

import gflags
import httplib2

from apiclient.discovery import build
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.tools import run

FLAGS = gflags.FLAGS

FLOW = OAuth2WebServerFlow(
    client_id=GOOGLE_API_CLIENT_ID,
    client_secret=GOOGLE_API_CLIENT_SECRET,
    scope=GOOGLE_API_CALENDAR_SCOPE,
    user_agent=GOOGLE_API_USER_AGENT)

storage = Storage(GOOGLE_API_CREDENTIAL_PATH)
credentials = storage.get()
if credentials is None or credentials.invalid == True:
  credentials = run(FLOW, storage)

http = httplib2.Http()
http = credentials.authorize(http)

service = build(serviceName='calendar', version='v3', http=http,
   developerKey=GOOGLE_API_DEVELOPER_KEY)

event = {
    [... Dictionary with all the necessary data here ...]
}

created_event = service.events().insert(calendarId=GOOGLE_API_CALENDAR_ID, body=event).execute()

This is pretty much the example from the Google documentation. The interesting bit is the Storage. It's a file where some credential data is saved.

Content of my storage file:

{
    "_module": "oauth2client.client", 
    "_class": "OAuth2Credentials", 
    "access_token": [redacted], 
    "token_uri": "https://accounts.google.com/o/oauth2/token", 
    "invalid": true, 
    "client_id": [redacted], 
    "client_secret": [redacted], 
    "token_expiry": "2011-12-17T16:44:15Z", 
    "refresh_token": null, 
    "user_agent": [redacted]
}

There should be a refresh token in there, but instead it's null. So I figure I can somehow request a refresh token.

I would appreciate any help on how I can get this to work. If you need more information, please tell me.

Kara
  • 6,115
  • 16
  • 50
  • 57
Jens
  • 20,533
  • 11
  • 60
  • 86

4 Answers4

19

I don't know how to do this with the Python Client or the Calendar API (I'm just using a ruby OAuth2 library for access to the Contacts API), but I found I needed to request "offline" access from the user.

This is done by adding the an "access_type" parameter with the value "offline" to the authorization url (the one you redirect the user to to click "I want to allow this application to get at my data").

If you do this, you get a refresh_token back in your JSON response when you ask google for access tokens. Otherwise you only get the access token (which is valid for an hour).

This requirement was apparently added recently (and may not be wonderfully documented).

You used to get a refresh token without having to get specific "offline" permission.

Hope this points you in the right direction.

bonkydog
  • 2,012
  • 20
  • 11
  • Thank you bonkydog! With the access_type parameter set to "offline" I got a refresh token. If the docs are right I do not need to manually create new tokens. – Jens Dec 18 '11 at 01:56
  • Cheers guys, this helped me out. – markdsievers Jan 26 '12 at 21:36
  • 1
    @Jens Can you please show a code sample to show how you solve your problem? I have the same problem. – tsil Jul 23 '12 at 23:18
  • @IsmaelToé I'm at work at the moment. I post an update later. – Jens Jul 24 '12 at 11:57
  • 1
    As of today (9/24/14) I am unable to get the refresh token to work using flow_from_clientsecrets, but it works fine with OAuth2WebServerFlow, just FYI – grokpot Sep 24 '14 at 23:31
14

So if you've already accepted consent without setting access_type='offline', you need to force the user to consent to your app with offline access by also passing approval_prompt='force'.

self.flow = OAuth2WebServerFlow(
    client_id=self.client_id,
    client_secret=self.client_secret,
    scope=self.SCOPE,
    user_agent=user_agent,
    redirect_uri='urn:ietf:wg:oauth:2.0:oob',
    access_type='offline', # This is the default
    approval_prompt='force'
)

Then you can remove the force prompt/set it to auto after you get the refresh token.

yellottyellott
  • 975
  • 1
  • 11
  • 18
  • Thanks for the answer @yellottyellott. But the questions was already answered over one year ago ;) But I upvoted it anyway since the `approval_prompt` bit was new and someone might be interested in that! Thank you. – Jens Jan 12 '13 at 11:20
  • Yeah, I know. I wasted too much time recently trying to get this to work before it clicked that all I needed to do was force the user to consent again. I wanted to include the snippet, otherwise I would have dropped it in the comments. Thanks for the question, it helped me! – yellottyellott Jan 13 '13 at 21:57
  • I'm attempting to do this and have replicated your settings above (thanks!) but it's still firing up a web browser and asking me to authenticate everytime. Also the "if credentials is None or credentials.invalid == True" line seems to always fire even if my creds are woefully expired. Been a really frustrating couple days, any ideas? Has this been updated perhaps? – user768680 Aug 07 '14 at 14:22
  • That last line is very important! I included approval_prompt='force' and never changed it to 'auto'. Hence forth it would always ask again even if I had the refresh token, which I dont want – Marc Maxmeister Feb 23 '16 at 14:47
5

The answers seems to be deprecated as of 2017-06-16.

  • approval_prompt is deprecated
  • prompt=force is not valid

Here is a working example:

from oauth2client.client import OAuth2WebServerFlow

flow = OAuth2WebServerFlow(
    client_id=CLIEN_ID,
    client_secret=CLIENT_SECRET,
    scope=SCOPES,
    redirect_uri='http://localhost/gdrive_auth',
    access_type='offline',
    prompt='consent',
)

print(flow.step1_get_authorize_url())
Szabolcs Dombi
  • 5,493
  • 3
  • 39
  • 71
1

If you follow this manual https://developers.google.com/identity/protocols/OAuth2WebServer and you can not receive refresh_token, try to add prompt='consent' here:

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',
    prompt='consent',
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes='true')

Here is described why it can happen

Anatoly E
  • 1,071
  • 11
  • 9