这个问题类似于Not receiving Google OAuth refresh token,但是我已经按照接受的解决方案的注释中的建议指定了access_type='offline'
。
我正在编写一个Django应用,以使用Google API发送日历邀请,这基本上是对https://developers.google.com/api-client-library/python/auth/web-app所提供的Flask示例的改编,其中我创建了一个模型GoogleCredentials
进行存储凭据持久存储在数据库中,而不是会话中。
以下是视图:
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&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
请注意,我已将access_type='offline'
传递给flow.authorization_url()
。这是GoogleCredentials
模型:
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()
在开发服务器运行的情况下,如果我转到localhost:8000/authorize
(已连接到authorize()
视图),然后我检查了第一个凭证,则发现refresh_token
是None
:
(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]: <QuerySet [<GoogleCredentials: GoogleCredentials object (2154)>]>
In [3]: gc = GoogleCredentials.objects.first()
In [4]: gc.__dict__
Out[4]:
{'_state': <django.db.models.base.ModelState at 0x111f91630>,
'created_at': datetime.datetime(2018, 8, 15, 17, 58, 33, 626971, tzinfo=<UTC>),
'updated_at': datetime.datetime(2018, 8, 15, 23, 8, 38, 634449, tzinfo=<UTC>),
'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']}
最初,这不是问题,但是过一会儿,如果我进入create_meeting()
视图,就会得到一个RefreshError
,我已经在{{ 1}}:
google.oauth2.credentials
换句话说,我需要一个@_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.')
来防止出现此错误。为什么在这种情况下Google API不返回一个?
答案 0 :(得分:4)
更仔细地遵循接受的答案,我发现可以通过删除Web应用程序对我的帐户的访问权限并重新添加来获得刷新令牌。我导航到https://myaccount.google.com/permissions,并删除了对“ Cleo”应用程序的访问权限:
然后我转到localhost:8000/authorize
(链接到authorize()
视图),再次查找保存的凭据,它们具有刷新令牌:
In [24]: from lucy_web.models import *
In [25]: gc = GoogleCredentials.objects.first()
In [26]: gc.__dict__
Out[26]:
{'_state': <django.db.models.base.ModelState at 0x109133e10>,
'created_at': datetime.datetime(2018, 8, 15, 17, 58, 33, 626971, tzinfo=<UTC>),
'updated_at': datetime.datetime(2018, 8, 16, 22, 37, 48, 108105, tzinfo=<UTC>),
'user_id': 2154,
'token': 'ya29.Glv6BbcPkVoFfoobarHGifJUlEKP7kvwO5G1myTDOw9UYfl1LKAGxt',
'refresh_token': '1/iafoobar4z1OxFtNljiLrmS0',
'token_uri': 'https://www.googleapis.com/oauth2/v3/token',
'client_id': '821409068013-unerntfoobarir12fus1o46.apps.googleusercontent.com',
'client_secret': 'bZt6lfoobarpI8Qj10y',
'scopes': ['https://www.googleapis.com/auth/calendar']}
答案 1 :(得分:0)
访问在 1 小时后被撤销,解决方法是使用 refresh_token
。但此令牌仅在第一个授权请求时提供(当出现允许日历的 UI 时)。要解决此问题,请在不保存令牌后:
refresh_token
refresh_token
在正常操作中,只需:
refresh_token
refresh_token
答案 2 :(得分:0)
使用 https://gist.github.com/fxyw/6759e1ef9b91aadc29c54248493af742
中的方法
改编自https://gist.github.com/ikai/5905078