| # Copyright (C) 2010 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Utilities for OAuth. |
| |
| Utilities for making it easier to work with OAuth. |
| """ |
| |
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
| |
| |
| import copy |
| import httplib2 |
| import logging |
| import oauth2 as oauth |
| import urllib |
| import urlparse |
| |
| from oauth2client.anyjson import simplejson |
| from oauth2client.client import Credentials |
| from oauth2client.client import Flow |
| from oauth2client.client import Storage |
| |
| try: |
| from urlparse import parse_qsl |
| except ImportError: |
| from cgi import parse_qsl |
| |
| |
| class Error(Exception): |
| """Base error for this module.""" |
| pass |
| |
| |
| class RequestError(Error): |
| """Error occurred during request.""" |
| pass |
| |
| |
| class MissingParameter(Error): |
| pass |
| |
| |
| class CredentialsInvalidError(Error): |
| pass |
| |
| |
| def _abstract(): |
| raise NotImplementedError('You need to override this function') |
| |
| |
| def _oauth_uri(name, discovery, params): |
| """Look up the OAuth URI 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 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 |
| self.store = None |
| |
| # True if the credentials have been revoked |
| self._invalid = False |
| |
| @property |
| def invalid(self): |
| """True if the credentials are invalid, such as being revoked.""" |
| return getattr(self, "_invalid", False) |
| |
| def set_store(self, store): |
| """Set the storage for the credential. |
| |
| Args: |
| store: callable, a callable that when passed a Credential |
| will store the credential back to where it came from. |
| This is needed to store the latest access_token if it |
| has been revoked. |
| """ |
| self.store = store |
| |
| def __getstate__(self): |
| """Trim the state down to something that can be pickled.""" |
| d = copy.copy(self.__dict__) |
| del d['store'] |
| return d |
| |
| def __setstate__(self, state): |
| """Reconstitute the state of the object from being pickled.""" |
| self.__dict__.update(state) |
| self.store = None |
| |
| def authorize(self, http): |
| """Authorize an httplib2.Http instance with these Credentials |
| |
| 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.""" |
| response_code = 302 |
| http.follow_redirects = False |
| while response_code in [301, 302]: |
| 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 is 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 |
| |
| resp, content = request_orig(uri, method, body, headers, |
| redirections, connection_type) |
| response_code = resp.status |
| if response_code in [301, 302]: |
| uri = resp['location'] |
| |
| # Update the stored credential if it becomes invalid. |
| if response_code == 401: |
| logging.info('Access token no longer valid: %s' % content) |
| self._invalid = True |
| if self.store is not None: |
| self.store(self) |
| raise CredentialsInvalidError("Credentials are no longer valid.") |
| |
| return resp, content |
| |
| http.request = new_request |
| return http |
| |
| |
| class TwoLeggedOAuthCredentials(Credentials): |
| """Two Legged Credentials object for OAuth 1.0a. |
| |
| The Two Legged object is created directly, not from a flow. Once you |
| authorize and httplib2.Http instance you can change the requestor and that |
| change will propogate to the authorized httplib2.Http instance. For example: |
| |
| http = httplib2.Http() |
| http = credentials.authorize(http) |
| |
| credentials.requestor = 'foo@example.info' |
| http.request(...) |
| credentials.requestor = 'bar@example.info' |
| http.request(...) |
| """ |
| |
| def __init__(self, consumer_key, consumer_secret, user_agent): |
| """ |
| Args: |
| consumer_key: string, An OAuth 1.0 consumer key |
| consumer_secret: string, An OAuth 1.0 consumer secret |
| user_agent: string, The HTTP User-Agent to provide for this application. |
| """ |
| self.consumer = oauth.Consumer(consumer_key, consumer_secret) |
| self.user_agent = user_agent |
| self.store = None |
| |
| # email address of the user to act on the behalf of. |
| self._requestor = None |
| |
| @property |
| def invalid(self): |
| """True if the credentials are invalid, such as being revoked. |
| |
| Always returns False for Two Legged Credentials. |
| """ |
| return False |
| |
| def getrequestor(self): |
| return self._requestor |
| |
| def setrequestor(self, email): |
| self._requestor = email |
| |
| requestor = property(getrequestor, setrequestor, None, |
| 'The email address of the user to act on behalf of') |
| |
| def set_store(self, store): |
| """Set the storage for the credential. |
| |
| Args: |
| store: callable, a callable that when passed a Credential |
| will store the credential back to where it came from. |
| This is needed to store the latest access_token if it |
| has been revoked. |
| """ |
| self.store = store |
| |
| def __getstate__(self): |
| """Trim the state down to something that can be pickled.""" |
| d = copy.copy(self.__dict__) |
| del d['store'] |
| return d |
| |
| def __setstate__(self, state): |
| """Reconstitute the state of the object from being pickled.""" |
| self.__dict__.update(state) |
| self.store = None |
| |
| def authorize(self, http): |
| """Authorize an httplib2.Http instance with these Credentials |
| |
| 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.""" |
| response_code = 302 |
| http.follow_redirects = False |
| while response_code in [301, 302]: |
| # add in xoauth_requestor_id=self._requestor to the uri |
| if self._requestor is None: |
| raise MissingParameter( |
| 'Requestor must be set before using TwoLeggedOAuthCredentials') |
| parsed = list(urlparse.urlparse(uri)) |
| q = parse_qsl(parsed[4]) |
| q.append(('xoauth_requestor_id', self._requestor)) |
| parsed[4] = urllib.urlencode(q) |
| uri = urlparse.urlunparse(parsed) |
| |
| req = oauth.Request.from_consumer_and_token( |
| self.consumer, None, http_method=method, http_url=uri) |
| req.sign_request(signer, self.consumer, None) |
| if headers is 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 |
| resp, content = request_orig(uri, method, body, headers, |
| redirections, connection_type) |
| response_code = resp.status |
| if response_code in [301, 302]: |
| uri = resp['location'] |
| |
| if response_code == 401: |
| logging.info('Access token no longer valid: %s' % content) |
| # Do not store the invalid state of the Credentials because |
| # being 2LO they could be reinstated in the future. |
| raise CredentialsInvalidError("Credentials are invalid.") |
| |
| return resp, content |
| |
| http.request = new_request |
| return http |
| |
| |
| class FlowThreeLegged(Flow): |
| """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. |
| |
| Args: |
| verifier: string, dict - either the verifier token, or a dictionary |
| of the query parameters to the callback, which contains |
| the oauth_verifier. |
| Returns: |
| The Credentials object. |
| """ |
| |
| 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) |