1
votes

I have calendar events being created successfully for various GSuite users, then I'd like to set a watch event. I'm able to do that successfully:

SCOPES = ['https://www.googleapis.com/auth/calendar']
SERVICE_ACCOUNT_FILE = BASE_DIR + 'path_to_json'

credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES)

delegated_user = credentials.with_subject(user)
delegated_service = googleapiclient.discovery.build('calendar', 'v3', credentials=delegated_user)


event = {
        'summary': '%s at %s' % (event_name, event_location),
        'description': 'description',
        'location': event_address,
        'extendedProperties': {
            "private": {
                'briefType': event_name,
            }
        },
        'start': {
            'dateTime': event_start_time.astimezone().isoformat(),
        },
        'end': {
            'dateTime': event_end_time.astimezone().isoformat(),
        }

    }


event = delegated_service.events().insert(calendarId='primary', body=event).execute()

watch_body = {
    'id': event['id'],
    'type': 'web_hook',
    'address': 'https://example.com/googleapi'
}

watch = delegated_service.events().watch(calendarId='primary', body=watch_body).execute()

I'm not quite sure how to receive the push notification. I've registered/added my domain (per https://developers.google.com/calendar/v3/push), and when I change the event in Google Calendar I'm getting an API Push notification, but I'm receiving a 403 error:

Dec 11 20:58:38 ip-xxx gunicorn[3104]:  - - [11/Dec/2019:20:58:38 +0000] "POST /googleapi HTTP/1.0" 403 1889 "-" "APIs-Google; (+https://developers.google.com/webmasters/APIs-Google.html)"

How do I authenticate in the view that process /googleapi to be able to actually receive the API notification from Google?

I've tried the following just to try to debug what I'm receiving from Google, but I get nothing besides the 403 error:

def GoogleAPIWatchView(request):

    SCOPES = ['https://www.googleapis.com/auth/calendar']
    SERVICE_ACCOUNT_FILE = BASE_DIR + 'path_to_json'

    credentials = service_account.Credentials.from_service_account_file(
            SERVICE_ACCOUNT_FILE, scopes=SCOPES)

    regex = re.compile('^HTTP_')
    headers = dict((regex.sub('', header), value) for (header, value) 
           in request.META.items() if header.startswith('HTTP_'))

    print(headers)
    print(request)

    return JsonResponse('success', status=200, safe=False)
3
Are you able to send requests to the end-point directly? (to GoogleAPIWatchView view?)JPG

3 Answers

1
votes

Your application is trying to access user data using a service account, this is right and it is intended to do it so, but because you did not granted this rights to do it you are receiving 403 server responses.

To be able to run your application using a service account as you are trying the service account has to have the following rights granted:

Domain wide delegation authority:

If the service account does not have delegation authority over the domain the applications using a service account can not access data on behalf of the users of the domain.

To enable it go to here:

  1. Select your project
  2. Select your service account or create a new one
  3. Click on 'Edit' to edit parameters of the service account
  4. Select 'Enable G Suite Domain-Wide Delegation

Your service account now has domain-wide delegation.

Next you'll need to set the scopes of your service account.

Service account scopes:

The administrator of the G Suite domain must follow this steps:

  1. Go to your G Suite domain admin console:
  2. Select Security from the list of controls
  3. Select Advanced settings from the list of options
  4. Select Manage API client access in the Authentication section
  5. In the Client Name field enter the service account's Client ID. You can find your service account's client ID in the Service accounts page
  6. In the One or More API Scopes field enter the list of scopes that your application should be granted access to. In this case: https://www.googleapis.com/auth/calendar
  7. Authorize

Google Documentation on domain-wide authority delegation and creation of service accounts:

https://developers.google.com/identity/protocols/OAuth2ServiceAccount

1
votes

As @JPG rightly pointed out, your api is rejecting non-authorized request - push notification from Google. And CSRF is most probable cause - unsafe operations (POST, PUT, DELETE) require csrf token to be present in request.

Of course, api clients from other domains cannot provide one, as it is mainly designed to protect cross-domain ajax requests from web-pages rather than non-interactive api calls.

As Google API cannot authorize as regular user while accessing view, we need the view to be open to everyone, and to allow POST requests from everyone we remove GoogleAPIWatchView from csrf check.

It still requires protection though.

To make sure request was actually made by Google API, wee can check presence / value of some headers Google API adds to requests:

Note: all these headers can be spoofed by malicious party. For additional check you can define custom token while subscribing to watch notifications. The token is included in the X-Goog-Channel-Token HTTP header in every notification message that your application receives for this channel.

...
CHANNEL_TOKEN = "mysecrettoken"
watch_body = {
    'id': event['id'],
    'type': 'web_hook',
    'address': 'https://example.com/googleapi',
    'token': CHANNEL_TOKEN
}
...

And in the view we can perform necessary checks to make sure request is from Google:

from django.http import HttpResponseForbidden

@csrf_exempt  # This skips csrf validation.
def GoogleAPIWatchView(request):
    # Validate request came from Google
    if not 'APIs-Google' in request.META.get('HTTP_USER_AGENT', ''):
        return HttpResponseForbidden()
    if not request.META.get('HTTP_X_GOOG_CHANNEL_ID'):
        return HttpResponseForbidden()
    if not request.META.get('HTTP_X_GOOG_CHANNEL_TOKEN') == CHANNEL_TOKEN:
        return HttpResponseForbidden()

    ...

In case of Django Rest Framework, you also need view available to everyone without authentication. If SessionAuthentication is not present in AUTHENTICATION_CLASSES - we can skip @csrf_extempt decorator as csrf check is not performed in that case.

@api_view(['POST'])
def google_push_notification(request):
    authentication_classes = []
    permission_classes = [AllowAny]  # or empty []
    # check google headers
    ...
0
votes

It seems like this HTTP 403 raised because of failed verification of CSRF token. So, use csrf_exempt--Django doc decorator to skip the verification process

#views.py
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt  # This skips csrf validation.
def GoogleAPIWatchView(request):
    ...
    # do something useful