Showing posts with label OAuth. Show all posts
Showing posts with label OAuth. Show all posts

Monday, December 24, 2012

Bridging OAuth 2.0 objects between GData and Discovery

My colleague +Takashi Matsuo and I recently gave a talk about using OAuth2Decorator (from the google-api-python-client library) with request handlers in Google App Engine. Shortly after, a Stack Overflow question sprung up asking about the right way to use the decorator and, as a follow up,  if the decorator could be used with the Google Apps Provisioning API. As I mentioned in my answer,
The Google Apps Provisioning API is a Google Data API...As a result, you'll need to use the gdata-python-client library to use the Provisioning API. Unfortunately, you'll need to manually convert from a oauth2client.client.OAuth2Credentials object to a gdata.gauth.OAuth2Token object to use the same token for either one.
Instead of making everyone and their brother write their own, I thought I'd take a stab at it and write about it here. The general philosophy I took was that the token subclass should be 100% based on an OAuth2Credentials object:
  • the token constructor simply takes an OAuth2Credentials object
  • the token refresh updates the OAuth2Credentials object set on the token
  • values of the current token can be updated directly from the OAuth2Credentials object set on the token
Starting from the top, we'll use two imports:
import httplib2
from gdata.gauth import OAuth2Token
The first is needed to refresh an OAuth2Credentials object using the mechanics native to google-api-python-client, and the second is needed so we may subclass the gdata-python-client native token class.

As I mentioned, the values should be updated directly from an OAuth2Credentials object, so in our constructor, we first initialize the values to None and then call our update method to actual set the values. This allows us to write less code, because, repeating is bad (I think someone told me that once?).
class OAuth2TokenFromCredentials(OAuth2Token):
  def __init__(self, credentials):
    self.credentials = credentials
    super(OAuth2TokenFromCredentials, self).__init__(None, None, None, None)
    self.UpdateFromCredentials()
We can get away with passing four Nones to the superclass constructor, as it only has four positional arguments: client_idclient_secret, scope,  and user_agent. Three of those have equivalents on the OAuth2Credentials object, but there is no place for scope because that part of the token exchange handled elsewhere (OAuth2WebServerFlow) in the google-api-python-client library.
  def UpdateFromCredentials(self):
    self.client_id = self.credentials.client_id
    self.client_secret = self.credentials.client_secret
    self.user_agent = self.credentials.user_agent
    ...
Similarly, the OAuth2Credentials object only implements the refresh part of the OAuth 2.0 flow, so only has the token URI, hence auth_urirevoke_uri and redirect_uri will not be set either. However, the token URI and the token data are the same for both.
    ...
    self.token_uri = self.credentials.token_uri
    self.access_token = self.credentials.access_token
    self.refresh_token = self.credentials.refresh_token
    ...
Finally, we copy the extra fields which may be set outside of a constructor:
    ...
    self.token_expiry = self.credentials.token_expiry
    self._invalid = self.credentials.invalid
Since OAuth2Credentials doesn't deal with all parts of the OAuth 2.0 process, we disable those methods from OAuth2Token that do.
  def generate_authorize_url(self, *args, **kwargs): raise NotImplementedError
  def get_access_token(self, *args, **kwargs): raise NotImplementedError
  def revoke(self, *args, **kwargs): raise NotImplementedError
  def _extract_tokens(self, *args, **kwargs): raise NotImplementedError
Finally, the last method which needs to be implemented is _refresh, which should refresh the OAuth2Credentials object and then update the current GData token after the refresh. Instead of using the passed in request object, we use one from httplib2 as we mentioned in imports.
  def _refresh(self, unused_request):
    self.credentials._refresh(httplib2.Http().request)
    self.UpdateFromCredentials()
After refreshing the OAuth2Credentials object, we can update the current token using the same method called in the constructor.

Using this class, we can simultaneously call a discovery-based API and a GData API:
from apiclient.discovery import build
from gdata.contacts.client import ContactsClient

service = build('calendar', 'v3', developerKey='...')

class MainHandler(webapp2.RequestHandler):
  @decorator.oauth_required
  def get(self):
    auth_token = OAuth2TokenFromCredentials(decorator.credentials)
    contacts_client = ContactsClient()
    auth_token.authorize(contacts_client)
    contacts = contacts_client.get_contacts()
    ...
    events = service.events().list(calendarId='primary').execute(
        http=decorator.http())
    ...

Thursday, November 17, 2011

Quick and Dirty: Santa's Coming

I have been wanting to write a post for awhile, but was travelling for a work event and while on the road I decided to be lazy.

Since I just so happen to use a few GData APIs occasionally in my day to day work, most of the post ideas revolve around quirky things I have done or want to do with the APIs. Also, due to my obscene love for Python, all my mashups seem to end up using the Python Client for GData.

Back-story: As I was finalizing travel and gifts for my winter holiday back home, I called an old friend to let him know I'd be home in 40 days. After relaying this information to a few other people, I noted to my girlfriend that it would be nice if a computer would remind me of the count every day. This is where this quick and dirty pair of scripts come in to remind me when Santa is coming.

Pre-work — Account Settings: To allow an app to make requests on my behalf, I signed up to Manage my Domain for use with Google Apps, etc. For illustration purposes, let's say I used http://example.com (in reality, I used a pre-existing App of mine, I really just needed an OAuth token for one time use, no real safety concerns there). After adding this domain in the management page, I am able to get my "OAuth Consumer Key" and "OAuth Consumer Secret" which we'll say are EXAMPLE_KEY and EXAMPLE_SECRET in this example. Also in the management page, I set my "OAuth 2.0 Redirect URIs" and made sure my app can serve that page (even if it is a 404). Again for illustration, let's pretend I used http://example.com/verify.

After doing this settings pre-work, I have two scripts to do the work for me.

First script — get the OAuth Token:
import gdata.calendar.client
import gdata.gauth

gcal = gdata.calendar.client.CalendarClient()
oauth_callback = 'http://example.com/verify'
scopes = ['https://www.google.com/calendar/feeds/']
consumer_key = 'EXAMPLE_KEY'
consumer_secret = 'EXAMPLE_SECRET'
request_token = gcal.get_oauth_token(scopes, oauth_callback,
                                     consumer_key, consumer_secret)
out_str = ('Please visit https://www.google.com/accounts/OAuthAuthorize'
           'Token?hd=default&oauth_token=%s' % request_token.token)
print out_str
follow = raw_input('Please entry the follow link after authorizing:\n')
gdata.gauth.authorize_request_token(request_token, follow)
gcal.auth_token = gcal.get_access_token(request_token)
print 'TOKEN:', gcal.auth_token.token
print 'TOKEN_SECRET:', gcal.auth_token.token_secret
This script "spoofs" the OAuth handshake by asking the user (me) to go directly to the OAuth Authorize page. After doing so and authorizing the App, I am redirected to http://example.com/verify with query parameters for oauth_verifier and oauth_token. These are then used by the gauth section of the GData library to finish the OAuth handshake. Once the handshake is complete, the script prints out a necessary token and token secret to be used by the second script. I would advise piping the output to a file, augmenting the script to write them to a file, or writing these down (this is a joke, please don't write down 40 plus character goop that was produced by your computer). For the next script, let's pretend our token is FAKE_TOKEN and our token secret is FAKE_TOKEN_SECRET.

Second script — insert the events:
# General libraries
from datetime import date
from datetime import timedelta

# Third-party libraries
import atom
import gdata.gauth
import gdata.calendar.client
import gdata.calendar.data

gcal = gdata.calendar.client.CalendarClient()
auth_token = gdata.gauth.OAuthHmacToken(consumer_key='EXAMPLE_KEY',
                                        consumer_secret='EXAMPLE_SECRET',
                                        token='FAKE_TOKEN',
                                        token_secret='FAKE_TOKEN_SECRET',
                                        auth_state=3)
gcal.auth_token = auth_token

today = date.today()
days_left = (date(year=2011, month=12, day=23) - today).days

while days_left >= 0:
    event = gdata.calendar.data.CalendarEventEntry()
    if days_left > 1:
        msg = '%s Days Until Home for Christmas' % days_left
    elif days_left == 1:
        msg = '1 Day Until Home for Christmas'
    elif days_left == 0:
        msg = 'Going Home for Christmas'
    event.title = atom.data.Title(msg)

    # When
    start_time = '2011-%02d-%02dT08:00:00.000-08:00' % (today.month, today.day)
    end_time = '2011-%02d-%02dT09:00:00.000-08:00' % (today.month, today.day)
    event.when.append(gdata.calendar.data.When(
        start=start_time,
        end=end_time,
        reminder=[gdata.data.Reminder(hours='1')]))

    gcal.InsertEvent(event)

    today += timedelta(days=1)
    days_left -= 1
This script first authenticates by using the key/secret pair for the application (retrieved from the settings page) and the key/secret pair for the user token (that we obtained from the first script). To authenticate, we explicitly construct an HMAC-SHA1 signed token in the final auth state (3) of two-legged OAuth and then set the token on our calendar client (gcal).

After authenticating, we start with today and figure out the number of days in the countdown given my return date of December 23, 2011. With these in hand, we can loop through until there are no days left, creating a CalendarEventEntry with title as the number of days left in the countdown and occurring from 8am to 9am PST (UTC -8). Notice also I include a gdata.data.Reminder so I get an email at 7am every morning (60 minutes before the event) updating my brain on the length of the countdown!

Cleanup: Be sure to go to your issued tokens page and revoke access to the App (e.g. http://example.com) after doing this to avoid any unwanted security issues.

References: I have never read this, but I'm sure the documentation on Registration for Web-Based Applications is very helpful.

Notes:
  • You can annoy other people by inviting them to these events for them as well. To do so, simply add the following two lines before inserting the event
    who_add = gdata.calendar.data.EventWho(email='name@mail.com')
    event.who.append(who_add)
  • Sometimes inserting an item results in a RedirectError, so it may be safer to try the insert multiple times with a helper function such as the following:
    def try_insert(attempts, gcal, event):
        from time import sleep
        from gdata.client import RedirectError
    
        while attempts > 0:
          try:
              gcal.InsertEvent(event)
              break
          except RedirectError:
              attempts -= 1
              sleep(3)
              pass
    
        if attempts == 0:
            print 'Insert "%s" failed' % event.title.text
  • In what I swear was a complete coincidence, v3 of the Calendar API was announced today. I will try to use the new documentation to redo this quick and dirty example with v3.