245 lines
7.6 KiB
Python
245 lines
7.6 KiB
Python
#!/usr/bin/python2.4
|
|
#
|
|
# Copyright 2010 Google Inc. All Rights Reserved.
|
|
|
|
"""Utilities for OAuth.
|
|
|
|
Utilities for making it easier to work with OAuth.
|
|
"""
|
|
|
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
|
|
|
import copy
|
|
import httplib2
|
|
import oauth2 as oauth
|
|
import urllib
|
|
import logging
|
|
|
|
try:
|
|
from urlparse import parse_qs, parse_qsl
|
|
except ImportError:
|
|
from cgi import parse_qs, parse_qsl
|
|
|
|
|
|
class Error(Exception):
|
|
"""Base error for this module."""
|
|
pass
|
|
|
|
|
|
class RequestError(Error):
|
|
"""Error occurred during request."""
|
|
pass
|
|
|
|
|
|
class MissingParameter(Error):
|
|
pass
|
|
|
|
|
|
def _abstract():
|
|
raise NotImplementedError('You need to override this function')
|
|
|
|
|
|
def _oauth_uri(name, discovery, params):
|
|
"""Look up the OAuth UR from the discovery
|
|
document and add query parameters based on
|
|
params.
|
|
|
|
name - The name of the OAuth URI to lookup, one
|
|
of 'request', 'access', or 'authorize'.
|
|
discovery - Portion of discovery document the describes
|
|
the OAuth endpoints.
|
|
params - Dictionary that is used to form the query parameters
|
|
for the specified URI.
|
|
"""
|
|
if name not in ['request', 'access', 'authorize']:
|
|
raise KeyError(name)
|
|
keys = discovery[name]['parameters'].keys()
|
|
query = {}
|
|
for key in keys:
|
|
if key in params:
|
|
query[key] = params[key]
|
|
return discovery[name]['url'] + '?' + urllib.urlencode(query)
|
|
|
|
|
|
class Credentials(object):
|
|
"""Base class for all Credentials objects.
|
|
|
|
Subclasses must define an authorize() method
|
|
that applies the credentials to an HTTP transport.
|
|
"""
|
|
|
|
def authorize(self, http):
|
|
"""Take an httplib2.Http instance (or equivalent) and
|
|
authorizes it for the set of credentials, usually by
|
|
replacing http.request() with a method that adds in
|
|
the appropriate headers and then delegates to the original
|
|
Http.request() method.
|
|
"""
|
|
_abstract()
|
|
|
|
|
|
class OAuthCredentials(Credentials):
|
|
"""Credentials object for OAuth 1.0a
|
|
"""
|
|
|
|
def __init__(self, consumer, token, user_agent):
|
|
"""
|
|
consumer - An instance of oauth.Consumer.
|
|
token - An instance of oauth.Token constructed with
|
|
the access token and secret.
|
|
user_agent - The HTTP User-Agent to provide for this application.
|
|
"""
|
|
self.consumer = consumer
|
|
self.token = token
|
|
self.user_agent = user_agent
|
|
|
|
def authorize(self, http):
|
|
"""
|
|
Args:
|
|
http - An instance of httplib2.Http
|
|
or something that acts like it.
|
|
|
|
Returns:
|
|
A modified instance of http that was passed in.
|
|
|
|
Example:
|
|
|
|
h = httplib2.Http()
|
|
h = credentials.authorize(h)
|
|
|
|
You can't create a new OAuth
|
|
subclass of httplib2.Authenication because
|
|
it never gets passed the absolute URI, which is
|
|
needed for signing. So instead we have to overload
|
|
'request' with a closure that adds in the
|
|
Authorization header and then calls the original version
|
|
of 'request()'.
|
|
"""
|
|
request_orig = http.request
|
|
signer = oauth.SignatureMethod_HMAC_SHA1()
|
|
|
|
# The closure that will replace 'httplib2.Http.request'.
|
|
def new_request(uri, method='GET', body=None, headers=None,
|
|
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
|
|
connection_type=None):
|
|
"""Modify the request headers to add the appropriate
|
|
Authorization header."""
|
|
req = oauth.Request.from_consumer_and_token(
|
|
self.consumer, self.token, http_method=method, http_url=uri)
|
|
req.sign_request(signer, self.consumer, self.token)
|
|
if headers == None:
|
|
headers = {}
|
|
headers.update(req.to_header())
|
|
if 'user-agent' in headers:
|
|
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
|
|
else:
|
|
headers['user-agent'] = self.user_agent
|
|
return request_orig(uri, method, body, headers,
|
|
redirections, connection_type)
|
|
|
|
http.request = new_request
|
|
return http
|
|
|
|
|
|
class FlowThreeLegged(object):
|
|
"""Does the Three Legged Dance for OAuth 1.0a.
|
|
"""
|
|
|
|
def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
|
|
**kwargs):
|
|
"""
|
|
discovery - Section of the API discovery document that describes
|
|
the OAuth endpoints.
|
|
consumer_key - OAuth consumer key
|
|
consumer_secret - OAuth consumer secret
|
|
user_agent - The HTTP User-Agent that identifies the application.
|
|
**kwargs - The keyword arguments are all optional and required
|
|
parameters for the OAuth calls.
|
|
"""
|
|
self.discovery = discovery
|
|
self.consumer_key = consumer_key
|
|
self.consumer_secret = consumer_secret
|
|
self.user_agent = user_agent
|
|
self.params = kwargs
|
|
self.request_token = {}
|
|
required = {}
|
|
for uriinfo in discovery.itervalues():
|
|
for name, value in uriinfo['parameters'].iteritems():
|
|
if value['required'] and not name.startswith('oauth_'):
|
|
required[name] = 1
|
|
for key in required.iterkeys():
|
|
if key not in self.params:
|
|
raise MissingParameter('Required parameter %s not supplied' % key)
|
|
|
|
def step1_get_authorize_url(self, oauth_callback='oob'):
|
|
"""Returns a URI to redirect to the provider.
|
|
|
|
oauth_callback - Either the string 'oob' for a non-web-based application,
|
|
or a URI that handles the callback from the authorization
|
|
server.
|
|
|
|
If oauth_callback is 'oob' then pass in the
|
|
generated verification code to step2_exchange,
|
|
otherwise pass in the query parameters received
|
|
at the callback uri to step2_exchange.
|
|
"""
|
|
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
|
|
client = oauth.Client(consumer)
|
|
|
|
headers = {
|
|
'user-agent': self.user_agent,
|
|
'content-type': 'application/x-www-form-urlencoded'
|
|
}
|
|
body = urllib.urlencode({'oauth_callback': oauth_callback})
|
|
uri = _oauth_uri('request', self.discovery, self.params)
|
|
|
|
resp, content = client.request(uri, 'POST', headers=headers,
|
|
body=body)
|
|
if resp['status'] != '200':
|
|
logging.error('Failed to retrieve temporary authorization: %s' % content)
|
|
raise RequestError('Invalid response %s.' % resp['status'])
|
|
|
|
self.request_token = dict(parse_qsl(content))
|
|
|
|
auth_params = copy.copy(self.params)
|
|
auth_params['oauth_token'] = self.request_token['oauth_token']
|
|
|
|
return _oauth_uri('authorize', self.discovery, auth_params)
|
|
|
|
def step2_exchange(self, verifier):
|
|
"""Exhanges an authorized request token
|
|
for OAuthCredentials.
|
|
|
|
verifier - either the verifier token, or a dictionary
|
|
of the query parameters to the callback, which contains
|
|
the oauth_verifier.
|
|
"""
|
|
|
|
if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
|
|
verifier = verifier['oauth_verifier']
|
|
|
|
token = oauth.Token(
|
|
self.request_token['oauth_token'],
|
|
self.request_token['oauth_token_secret'])
|
|
token.set_verifier(verifier)
|
|
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
|
|
client = oauth.Client(consumer, token)
|
|
|
|
headers = {
|
|
'user-agent': self.user_agent,
|
|
'content-type': 'application/x-www-form-urlencoded'
|
|
}
|
|
|
|
uri = _oauth_uri('access', self.discovery, self.params)
|
|
resp, content = client.request(uri, 'POST', headers=headers)
|
|
if resp['status'] != '200':
|
|
logging.error('Failed to retrieve access token: %s' % content)
|
|
raise RequestError('Invalid response %s.' % resp['status'])
|
|
|
|
oauth_params = dict(parse_qsl(content))
|
|
token = oauth.Token(
|
|
oauth_params['oauth_token'],
|
|
oauth_params['oauth_token_secret'])
|
|
|
|
return OAuthCredentials(consumer, token, self.user_agent)
|