我正在将https://developers.google.com/api-client-library/python/auth/web-app中的Flask示例改编为Django应用。我有一个关于revoke
视图的问题,在Flask中,它是:
@app.route('/revoke')
def revoke():
if 'credentials' not in flask.session:
return ('You need to <a href="/authorize">authorize</a> before ' +
'testing the code to revoke credentials.')
credentials = google.oauth2.credentials.Credentials(
**flask.session['credentials'])
revoke = requests.post('https://accounts.google.com/o/oauth2/revoke',
params={'token': credentials.token},
headers = {'content-type': 'application/x-www-form-urlencoded'})
status_code = getattr(revoke, 'status_code')
if status_code == 200:
return('Credentials successfully revoked.' + print_index_table())
else:
return('An error occurred.' + print_index_table())
请注意,在Flask示例中,具有传递给google.oauth2.credentials.Credentials
构造函数所需信息的字典已保存在会话中,但是在代码示例中建议将其持久存储在实际应用程序中。 / p>
我想知道的是:撤销令牌后,在此示例中我们应该不这样做
del flask.session['credentials']
要防止/authorize
视图在下一次遇到撤销的凭证吗?
我问这个问题的原因是因为在我的Django应用程序中,我得到了RefreshError
:
RefreshError at /create-meeting
('invalid_grant: Token has been expired or revoked.', '{\n "error": "invalid_grant",\n "error_description": "Token has been expired or revoked."\n}')
这是我的示例代码:
import datetime
import requests
from django.utils import timezone
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
from django.template.response import SimpleTemplateResponse, HttpResponse
import google.oauth2.credentials
import google_auth_oauthlib.flow
import googleapiclient.discovery
from rest_framework import status
from lucy_web.models import GoogleCredentials
from lucy_web.utils import GoogleEvent
# 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):
service = get_service(user=request.user)
if not service:
return redirect(to=reverse('authorize'))
calendars = service.calendarList().list().execute()
return JsonResponse(calendars)
@login_required
def revoke(request):
"""
Revoke the permissions that the user has granted to the application.
"""
credentials = get_credentials(user=request.user)
if not credentials:
return HttpResponse(
f"No credentials found for user '{user}'. "
f"You need to <a href='{reverse('authorize')}'>authorize</a> before revoking credentials.")
revoke = requests.post(
'https://accounts.google.com/o/oauth2/revoke',
params={'token': credentials.token},
headers={'content-type': 'application/x-www-form-urlencoded'})
if revoke.status_code == status.HTTP_200_OK:
return HttpResponse(f"Credentials successfully revoked for user '{user}'.")
else:
return HttpResponse(f"An error occurred. (Status: {revoke.status_code})")
def get_service(user):
"""
Build a service object which can make authorized API requests on behalf of the user.
"""
credentials = get_credentials(user=user)
if not credentials:
# Return None if no credentials are found in the database.
# In this case, the user should be redirected to the authorization flow.
return
return googleapiclient.discovery.build(
API_SERVICE_NAME, API_VERSION, credentials=credentials)
def get_credentials(user):
"""
Construct a google.oauth2.credentials.Credentials object from the
user's GoogleCredentials stored in the database.
"""
try:
return google.oauth2.credentials.Credentials(
**GoogleCredentials.objects.get(user=user).to_dict())
except GoogleCredentials.DoesNotExist:
return
def save_credentials(user, credentials):
"""
Store a user's google.oauth2.credentials.Credentials in the datbase.
"""
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
# import ipdb; ipdb.set_trace()
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',
# prompt='select_account',
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
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) # noqa: E501
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.pyself. # noqa: E501
(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):
"""
Update the user's credentials' from a google.oauth2.credentials.Credentials object
"""
self.token = credentials.token
if credentials.refresh_token is not None:
# The refresh token is only provided on the first authorization from the user
# (cf. https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token)
# To troubleshoot a RefreshError, try visiting https://myaccount.google.com/permissions,
# removing the Cleo app's access, and adding it again by visiting the 'authorize' view
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()
遇到此错误时,我注意到的是,如果我删除Django shell中的凭据,则:
In [45]: gc = GoogleCredentials.objects.first()
In [46]: gc
Out[46]: <GoogleCredentials: GoogleCredentials object (2154)>
In [47]: gc.delete()
Out[47]: (1, {'lucy_web.GoogleCredentials': 1})
然后,当我再次访问localhost:8000/create-meeting
(链接到create_meeting
视图)时,系统会要求我授予应用程序管理日历的授权,并且以后可以查看我的日历列表
我唯一的疑问是我想知道为什么Google示例未删除凭据?