title: Google OAuth2 not issuing a refresh token even with access_type='offline'?
body: <p>This question is similar to <a href="https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token">Not receiving Google OAuth refresh token</a>, but I have already specified <code>access_type='offline'</code> as suggested in the comments to the accepted solution.</p>
  
  <p>I'm writing a Django app to send calendar invites using the Google API which is basically an adaptation of the Flask example given at <a href="https://developers.google.com/api-client-library/python/auth/web-app" rel="noreferrer">https://developers.google.com/api-client-library/python/auth/web-app</a>, in which I've created a model <code>GoogleCredentials</code> to store credentials persistently in a database instead of in the session.</p>
  
  <p>Here are the views:</p>
  
  <pre><code>import logging
  from django.conf import settings
  from django.shortcuts import redirect
  from django.http import JsonResponse
  from django.urls import reverse
  from django.contrib.auth.decorators import login_required
  import google.oauth2.credentials
  import google_auth_oauthlib.flow
  import googleapiclient.discovery
  from lucy_web.models import GoogleCredentials
  
  logger = logging.getLogger(__name__)
  
  
  # Client configuration for an OAuth 2.0 web server application
  # (cf. https://developers.google.com/identity/protocols/OAuth2WebServer)
  # This is constructed from environment variables rather than from a
  # client_secret.json file, since the Aptible deployment process would
  # require us to check that into version control, which is not in accordance
  # with the 12-factor principles.
  # The client_secret.json containing this information can be downloaded from
  # https://console.cloud.google.com/apis/credentials?organizationId=22827866999&amp;project=cleo-212520
  CLIENT_CONFIG = {'web': {
      'client_id': settings.GOOGLE_CLIENT_ID,
      'project_id': settings.GOOGLE_PROJECT_ID,
      'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
      'token_uri': 'https://www.googleapis.com/oauth2/v3/token',
      'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
      'client_secret': settings.GOOGLE_CLIENT_SECRET,
      'redirect_uris': settings.GOOGLE_REDIRECT_URIS,
      'javascript_origins': settings.GOOGLE_JAVASCRIPT_ORIGINS}}
  
  # This scope will allow the application to manage the user's calendars
  SCOPES = ['https://www.googleapis.com/auth/calendar']
  API_SERVICE_NAME = 'calendar'
  API_VERSION = 'v3'
  
  
  @login_required
  def authorize(request):
      authorization_url, state = _get_authorization_url(request)
      request.session['state'] = state
      return redirect(to=authorization_url)
  
  
  @login_required
  def oauth2callback(request):
      flow = _get_flow(request, state=request.session['state'])
  
      # Note: to test this locally, set OAUTHLIB_INSECURE_TRANSPORT=1 in your .env file
      # (cf. https://stackoverflow.com/questions/27785375/testing-flask-oauthlib-locally-without-https)
      flow.fetch_token(authorization_response=request.get_raw_uri())
      _save_credentials(user=request.user, credentials=flow.credentials)
      return redirect(to=reverse('create-meeting'))
  
  
  @login_required
  def create_meeting(request):
      # Retrieve the user's credentials from the database, redirecting
      # to the authorization page if none are found
      credentials = _get_credentials(user=request.user)
      if not credentials:
          return redirect(to=reverse('authorize'))
  
      calendar = googleapiclient.discovery.build(
          API_SERVICE_NAME, API_VERSION, credentials=credentials)
  
      calendars = calendar.calendarList().list().execute()
  
      return JsonResponse(calendars)
  
  
  def _get_credentials(user):
      """
      Retrieve a user's google.oauth2.credentials.Credentials from the database.
      """
      try:
          _credentials = GoogleCredentials.objects.get(user=user)
      except GoogleCredentials.DoesNotExist:
          return
  
      return google.oauth2.credentials.Credentials(**_credentials.to_dict())
  
  
  def _save_credentials(user, credentials):
      """
      Store a user's google.oauth2.credentials.Credentials in the database.
      """
      gc, _ = GoogleCredentials.objects.get_or_create(user=user)
      gc.update_from_credentials(credentials)
  
  
  def _get_authorization_url(request):
      flow = _get_flow(request)
  
      # Generate URL for request to Google's OAuth 2.0 server
      return 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',
          login_hint=settings.SCHEDULING_EMAIL,
          # Enable incremental authorization. Recommended as a best practice.
          include_granted_scopes='true')
  
  
  def _get_flow(request, **kwargs):
      # Use the information in the client_secret.json to identify
      # the application requesting authorization.
      flow = google_auth_oauthlib.flow.Flow.from_client_config(
          client_config=CLIENT_CONFIG,
          scopes=SCOPES,
          **kwargs)
  
      # Indicate where the API server will redirect the user after the user completes
      # the authorization flow. The redirect URI is required.
      flow.redirect_uri = request.build_absolute_uri(reverse('oauth2callback'))
      return flow
  </code></pre>
  
  <p>Note that I have passed <code>access_type='offline'</code> to the <code>flow.authorization_url()</code>. Here is the <code>GoogleCredentials</code> model:</p>
  
  <pre><code>from django.db import models
  from django.contrib.postgres.fields import ArrayField
  from .timestamped_model import TimeStampedModel
  from .user import User
  
  
  class GoogleCredentials(TimeStampedModel):
      """
      Model for saving Google credentials to a persistent database (cf. https://developers.google.com/api-client-library/python/auth/web-app)
      The user's ID is used as the primary key, following https://github.com/google/google-api-python-client/blob/master/samples/django_sample/plus/models.py.
      (Note that we don't use oauth2client's CredentialsField as that library is deprecated).
      """
      user = models.OneToOneField(
          User,
          primary_key=True,
          limit_choices_to={'is_staff': True},
          # Deleting a user will automatically delete his/her Google credentials
          on_delete=models.CASCADE)
      token = models.CharField(max_length=255, null=True)
      refresh_token = models.CharField(max_length=255, null=True)
      token_uri = models.CharField(max_length=255, null=True)
      client_id = models.CharField(max_length=255, null=True)
      client_secret = models.CharField(max_length=255, null=True)
      scopes = ArrayField(models.CharField(max_length=255), null=True)
  
      def to_dict(self):
          """
          Return a dictionary of the fields required to construct
          a google.oauth2.credentials.Credentials object
          """
          return dict(
              token=self.token,
              refresh_token=self.refresh_token,
              token_uri=self.token_uri,
              client_id=self.client_id,
              client_secret=self.client_secret,
              scopes=self.scopes)
  
      def update_from_credentials(self, credentials):
          self.token = credentials.token
          self.refresh_token = credentials.refresh_token
          self.token_uri = credentials.token_uri
          self.client_id = credentials.client_id
          self.client_secret = credentials.client_secret
          self.scopes = credentials.scopes
          self.save()
  </code></pre>
  
  <p>With the development server running, if I go to <code>localhost:8000/authorize</code> (which is hooked up to the <code>authorize()</code> view) and I afterwards check the first credential, I see that the <code>refresh_token</code> is <code>None</code>:</p>
  
  <pre><code>(lucy-web-CVxkrCFK) bash-3.2$ python manage.py shell
  Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) 
  Type 'copyright', 'credits' or 'license' for more information
  IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.
  
  In [1]: from lucy_web.models import *
  
  In [2]: GoogleCredentials.objects.all()
  Out[2]: &lt;QuerySet [&lt;GoogleCredentials: GoogleCredentials object (2154)&gt;]&gt;
  
  In [3]: gc = GoogleCredentials.objects.first()
  
  In [4]: gc.__dict__
  Out[4]: 
  {'_state': &lt;django.db.models.base.ModelState at 0x111f91630&gt;,
   'created_at': datetime.datetime(2018, 8, 15, 17, 58, 33, 626971, tzinfo=&lt;UTC&gt;),
   'updated_at': datetime.datetime(2018, 8, 15, 23, 8, 38, 634449, tzinfo=&lt;UTC&gt;),
   'user_id': 2154,
   'token': 'ya29foobar6tA',
   'refresh_token': None,
   'token_uri': 'https://www.googleapis.com/oauth2/v3/token',
   'client_id': '8214foobar13-unernto9l5ievs2pi0l6fir12fus1o46.apps.googleusercontent.com',
   'client_secret': 'bZt6foobarQj10y',
   'scopes': ['https://www.googleapis.com/auth/calendar']}
  </code></pre>
  
  <p>Initially, this is not a problem, but after a while, if I go to the <code>create_meeting()</code> view, I get a <code>RefreshError</code> which I've traced to this bit of source code in <code>google.oauth2.credentials</code>:</p>
  
  <pre><code>@_helpers.copy_docstring(credentials.Credentials)
  def refresh(self, request):
      if (self._refresh_token is None or
              self._token_uri is None or
              self._client_id is None or
              self._client_secret is None):
          raise exceptions.RefreshError(
              'The credentials do not contain the necessary fields need to '
              'refresh the access token. You must specify refresh_token, '
              'token_uri, client_id, and client_secret.')
  </code></pre>
  
  <p>In other words, I need a <code>refresh_token</code> to prevent this error. Why is the Google API not returning one in this case?</p>