Django 1.7 google oauth2 token validation failure
Asked Answered
F

2

8

I'm trying to get through the process of authenticating a Google token for accessing a user's calendar within a Django application. Although I've followed several indications found on the web, I'm stuck with a 400 error code response to my callback function (Bad Request).

views.py

# -*- coding: utf-8 -*-
import os

import argparse
import httplib2
import logging

from apiclient.discovery import build
from oauth2client import tools
from oauth2client.django_orm import Storage
from oauth2client import xsrfutil
from oauth2client.client import flow_from_clientsecrets

from django.http import HttpResponse
from django.http import HttpResponseBadRequest
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from django.conf import settings

from apps.tecnico.models import Credentials, Flow

CLIENT_SECRETS = os.path.join(
    os.path.dirname(__file__), '../../client_secrets.json')

@login_required
def index(request):
    storage = Storage(Credentials, 'id', request.user, 'credential')
    FLOW = flow_from_clientsecrets(
        CLIENT_SECRETS,
        scope='https://www.googleapis.com/auth/calendar.readonly',
        redirect_uri='http://MY_URL:8000/oauth2/oauth2callback'
    )
    credential = storage.get()
    if credential is None or credential.invalid is True:
        FLOW.params['state'] = xsrfutil.generate_token(
            settings.SECRET_KEY, request.user)
        authorize_url = FLOW.step1_get_authorize_url()
        f = Flow(id=request.user, flow=FLOW)
        f.save()
        return HttpResponseRedirect(authorize_url)
    else:
        http = httplib2.Http()
        http = credential.authorize(http)
        service = build(serviceName='calendar', version='v3', http=http,
                        developerKey='MY_DEV_KEY_FROM_GOOGLE_CONSOLE')

        events = service.events().list(calendarId='primary').execute()
        return render_to_response('calendario/welcome.html', {
            'events': events['items'],
        })


@login_required
def auth_return(request):
    if not xsrfutil.validate_token(
            settings.SECRET_KEY, request.REQUEST['state'], request.user):
        return HttpResponseBadRequest()

    storage = Storage(Credentials, 'id', request.user, 'credential')
    FLOW = Flow.objects.get(id=request.user).flow
    credential = FLOW.step2_exchange(request.REQUEST)
    storage.put(credential)
    return HttpResponseRedirect("http://MY_URL:8000/caly")

models.py

from oauth2client.django_orm import FlowField, CredentialsField

[...]

class Credentials(models.Model):
    id = models.ForeignKey(User, primary_key=True)
    credential = CredentialsField()


class Flow(models.Model):
    id = models.ForeignKey(User, primary_key=True)
    flow = FlowField()

I've downloaded the client_secrets.json file directly from the Google Dev Console. The specified Client ID type in the Dev Console is "web application", which I think is correct. What I've noticed is, if I remove the token validation code block:

if not xsrfutil.validate_token(
        settings.SECRET_KEY, request.REQUEST['state'], request.user):
    return HttpResponseBadRequest()

everything works correctly, flow and credentials get correctly stored in the database and I'm allowed to read the calendar. What can I possibly be wrong with?

EDIT: I've also checked outgoing (to Google) and incoming (to callback) data:

OUTGOING:

request.user:
admin
settings.SECRET_KEY:
I_AM_NOT_WRITING_IT_HERE
FLOW.params['state']:
SOME_OTHER_RANDOM_STUFF

INCOMING:

request.user:
admin
settings.SECRET_KEY:
I_AM_NOT_WRITING_IT_HERE
FLOW.params['state']:
SOME_OTHER_RANDOM_STUFF

Data is identical, at least to a print to console. Also, the generation/validation operations via console work correctly (xsrfutil.validate_token returns True, both with test and real data, including User model instances). I'm even more puzzled.

Fretwork answered 12/12, 2014 at 10:25 Comment(4)
I've just run across this bug myself and am seeing exactly the same symptoms. I believe there is a bug in the generate/validate routines around the (optional) time value. Will post if I find something definitive.Arturoartus
Thanks @John Pirie, I'm still definitely looking forward to solving this. I had to give up for now as I have other parts of my software to develop, but if you find anything useful, please let me know.Fretwork
It looks to me like xsrfutil.validate_token is seeing an error thrown on its call to urlsafe_base64decode(), which causes the validate to always return False. Seems like a bug in validate_token, but I wasn't able to figure out root cause, and have not contacted the developer. I am removing the xsrfutil code for now, and will try to use Django's built-in csrf_token features instead.Arturoartus
Thanks to everyone who contributed in this. Unfortunately right now I can't check myself if the solution originally proposed by @Ryan Spaulding works. As soon as I'll get back on that part of my project I'll give it a crack and eventually accept that answer, proven it ultimately resolves the issue.Fretwork
O
14

I have struggled exact the same issue for several hours, and I figured out the solution of which @Ryan Spaulding and @Hans Z answered. It works!

This is due to the fact Django 1.7 returns a unicode object for the state variable above using request.REQUEST. I was previously using Django 1.6 which used to return a string.

One can find more detail here. https://github.com/google/google-api-python-client/issues/58 I wrote this post for future reference.

if not xsrfutil.validate_token(
    settings.SECRET_KEY, 
    str(request.REQUEST['state']), 
    request.user):
return HttpResponseBadRequest()
Oxidase answered 10/3, 2015 at 16:44 Comment(3)
Nice, I'm guessing this saved multiple hours for me. Thanks!Diagraph
After several months, as I won't probably get much chance to try out this suggestion directly any more, I'm accepting this answer as it looks like the community stated that it's actually the right path being pointed out here. Thanks @wthrain, and sorry for the lateness.Fretwork
For newer Django users (v1.9+), note that request.REQUEST is deprecated per this ticket, and so you want request.GET['state'] instead.Coleorhiza
R
7

It could be a unicode issue with request.REQUEST['state']. Try putting str() around it, i.e. str(request.REQUEST['state']).

Rafael answered 27/1, 2015 at 22:36 Comment(1)
Please format your answer with some code formattingSicanian

© 2022 - 2024 — McMap. All rights reserved.