Decorating Django views that require Google Authorization with python decorators

In a project that I am currently involved, we needed to authenticate with Google Analytics in order to query statistics. As you may know currently Google supports three authentication mechanisms to allow other applications to authenticate on someone’s behalf.

  • ClientLogin: You give your username, password and the application does the rest for you. I don’t think there are still people out there who can give away their passwords to applications to be used on their behalf.
  • AuthSub: Google’s proprietary authorization API, just available as an alternative to OAuth.
  • OAuth: Provides authorization for all Google APIs and Google also suggests to migrate application that uses to OAuth.

As can be seen from the list that there are not so many logical alternatives, the suggested way is to use OAuth which is also an open standard. After deciding which authorization API to use, I used some time to investigate the usage and find some practical examples. However I could not find anything that fits to my needs completely.

  • I found one but that was using gdata v1.
  • I found another one but it was not using Django.
  • I found one that uses Django but I did not like the way it is designed.

Anyway, I decided to write my own. I spent some time thinking the best way to implement something that is pluggable and does not do stupid redirects between views and I decided to write a decorator that can be applied to Django views. I mainly choose to do so because of the following reasons, please comment on it if you think my reasoning is not right or there is a better way to do it.

  • It is declarative, means; it is pluggable
  • It is clean, it does not pollutes the actual view code
  • In place configuration, you can also accompany the configuration while applying it

So what I did was;

  • Creating a decorator to do the authorization
  • Creating a helper containing functions to load/save received tokens
  • Applying the decorators wherever necessary

The following is the source of the decorator.

  • It checks if there is an access token matching the provided settings
    • If so return the original view function
    • If not check if there is a matching request token
      • If so upgrade the request token to an access token and return the original view function
      • If not get a request token, redirect to the generated authorization url while setting the callback url to the url of the about to be executed view

What is important is that, after a successful authorization the user will return to this view again since we are setting the callback url accordingly before redirecting the user to Google.

import logging
try:
    from functools import wraps
except ImportError:
    from django.utils.functional import wraps

from django.utils.decorators import available_attrs

import gdata

from helpers import *

logger = logging.getLogger(__name__)

def secure_with_gauth(view_func=None, token_prefix='default', scopes=['https://docs.google.com/feeds/'],
                      consumer_key='anonymous', consumer_secret='anonymous', consumer_source = 'www.foo.com',
                      error_callback=None):
    """
    Wraps the actual view method and makes authorization for google services according to the paremters provided.
    """
    def decorator(view_func):

        @wraps(view_func, assigned=available_attrs(view_func))

        def _wrapped_view(request, *args, **kwargs):

            from django.shortcuts import redirect
            access_token = load_token(token_prefix, True)
            saved_request_token = load_token(token_prefix, False)

            if not isinstance(access_token, gdata.gauth.OAuthHmacToken):
                logger.info("Access token does not exists.")

                if isinstance(saved_request_token, gdata.gauth.OAuthHmacToken):
                    client = gdata.client.GDClient(source=consumer_source)

                    try:
                        request_token = gdata.gauth.AuthorizeRequestToken(saved_request_token,
                                                                          request.build_absolute_uri())
                        access_token = client.GetAccessToken(request_token)
                        save_token(access_token, token_prefix, True)
                    except gdata.client.RequestError:
                        if error_callback:
                            remove_token(token_prefix, False)
                            return error_callback(request, "Error while upgrading request token to authorization \
                            token, please try again later.")

                    logger.info("Successfully retrieved access token, returning the view.")

                    return view_func(request, *args, **kwargs)
                else:
                    logger.info("Request token does not exist")

                    client = gdata.client.GDClient(source=consumer_source)

                    try:
                        request_token = client.GetOAuthToken(scopes, request.build_absolute_uri(), consumer_key,
                                                             consumer_secret)
                        save_token(request_token, token_prefix, False)
                    except gdata.client.RequestError:
                        if error_callback:
                            return error_callback(request, "Error while getting request token, please try again later.")

                    logger.info("Successfully retrieved request token, redirecting to authorization url.")

                    return redirect(request_token.generate_authorization_url().__str__())

            return view_func(request, *args, **kwargs)

        return _wrapped_view

    return decorator

Applying the decorator is also very straight forward. See the following usage within views.py.

@secure_with_gauth(token_prefix=TOKEN_PREFIX, scopes=['https://www.google.com/analytics/feeds'],
                   consumer_source=CONSUMER_SOURCE, consumer_key=CONSUMER_KEY, consumer_secret=CONSUMER_SECRET,
                   error_callback=generate_error)
def google_analytics(request, type):

I hope it helps.