撤销Google凭据时,是否应该删除保存的凭据?

时间:2018-08-22 17:34:03

标签: python django flask

我正在将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示例未删除凭据?

0 个答案:

没有答案