# -*- coding: utf-8 -*- ''' rauth.service ------------- Provides OAuth 1.0/a, 2.0 and Ofly service containers. ''' from rauth.compat import urlencode from rauth.session import OAuth1Session, OAuth2Session, OflySession from rauth.utils import ENTITY_METHODS, parse_utf8_qsl PROCESS_TOKEN_ERROR = ('Decoder failed to handle {key} with data as returned ' 'by provider. A different decoder may be needed. ' 'Provider returned: {raw}') def process_token_request(r, decoder, *args): try: data = decoder(r.content) return tuple(data[key] for key in args) except KeyError as e: # pragma: no cover bad_key = e.args[0] raise KeyError(PROCESS_TOKEN_ERROR.format(key=bad_key, raw=r.content)) class Service(object): __attrs__ = ['name', 'base_url', 'authorize_url'] def __init__(self, name, base_url, authorize_url): #: The service name, e.g. 'twitter'. self.name = name #: The base URL used for making API requests. self.base_url = base_url #: The authorization URL. self.authorize_url = authorize_url def __getstate__(self): return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__) def __setstate__(self, state): for attr, value in state.items(): setattr(self, attr, value) class OAuth1Service(Service): ''' An OAuth 1.0/a Service container. This class provides a wrapper around a specialized Requests' :class:`~requests.sessions.Session` object. Primarily this wrapper is used to produce authenticated session objects. These may be used to make requests against OAuth 1.0/a endpoints. You might intialize :class:`OAuth1Service` something like this:: service = OAuth1Service( name='example', consumer_key='123', consumer_secret='456', request_token_url='http://example.com/request_token', access_token_url='http://example.com/access_token', authorize_url='http://example.com/authorize', base_url='http://example.com/api') Now the request token should be retrieved:: request_token, request_token_secret = service.get_request_token() .. admonition:: Differing Request Token Formats Some services provide different formatting when returning tokens. For this reason the service wrapper provides a special method :meth:`get_raw_request_token`. This will return the unparsed response. At this point it's up to you to extract the necessary data. It's time to access the authorize URI and direct the client to authorize requests on their behalf. This URI is retrieved as follows:: authorize_url = service.get_authorize_url(request_token) Once the client has authorized the request it is now possible to retrieve an access token. Do so as follows:: session = service.get_auth_session(request_token, request_token_secret) .. admonition:: Differing Access Token Formats Some services provide different formatting when returning tokens. For this reason the service wrapper provides a special method :meth:`get_raw_access_token`. This will return the unparsed response. At this point it's up to you to extract the necessary data. Finally we have an authenticated session and are ready to make requests against OAuth 1.0/a endpoints. Because Rauth is a wrapper around Requests, the same API you would use with Requests is exposed and expected:: r = session.get('some/resource/', params={'format': 'json'}) print r.json() :param consumer_key: Client consumer key, required for signing. :type consumer_key: str :param consumer_secret: Client consumer secret, required for signing. :type consumer_secret: str :param name: The service name, defaults to `None`. :type name: str :param request_token_url: Request token endpoint, defaults to `None`. :type request_token_url: str :param access_token_url: Access token endpoint, defaults to `None`. :type access_token_url: str :param authorize_url: Authorize endpoint, defaults to `None`. :type authorize_url: str :param base_url: A base URL from which to construct requests, defaults to `None`. :type base_url: str :param session_obj: Object used to construct sessions with, defaults to :class:`rauth.OAuth1Session ` :type session_obj: :class:`Session` :param signature_obj: Object used to construct signatures with, defaults to :class:`rauth.oauth.HmacSha1Signature ` :type signature_obj: :class:`SignatureMethod` ''' __attrs__ = Service.__attrs__ + ['consumer_key', 'consumer_secret', 'request_token_url', 'access_token_url', 'session_obj'] def __init__(self, consumer_key, consumer_secret, name=None, request_token_url=None, access_token_url=None, authorize_url=None, base_url=None, session_obj=None, signature_obj=None): #: Client credentials. self.consumer_key = consumer_key self.consumer_secret = consumer_secret #: Authorization endpoints. self.request_token_url = request_token_url self.access_token_url = access_token_url #: Object used to construct sessions with. self.session_obj = session_obj or OAuth1Session #: Object used to construct signatures with. self.signature_obj = signature_obj #: Request and access token responses. self.request_token_response = None self.access_token_response = None super(OAuth1Service, self).__init__(name, base_url, authorize_url) def get_session(self, token=None, signature=None): ''' If provided a `token` parameter, tries to retrieve a stored `rauth.OAuth1Session` instance. Otherwise generates a new session instance with the :class:`rauth.OAuth1Service.consumer_key` and :class:`rauth.OAuth1Service.consumer_secret` stored on the `rauth.OAuth1Service` instance. :param token: A tuple of strings with which to memoize the session object instance. :type token: tuple ''' if token is not None: access_token, access_token_secret = token session = self.session_obj(self.consumer_key, self.consumer_secret, access_token, access_token_secret, signature or self.signature_obj, service=self) else: # pragma: no cover signature = signature or self.signature_obj session = self.session_obj(self.consumer_key, self.consumer_secret, signature=signature, service=self) return session def get_raw_request_token(self, method='GET', **kwargs): ''' Returns a Requests' response over the :attr:`rauth.OAuth1Service.request_token_url`. Use this if your endpoint if you need the full `Response` object. :param method: A string representation of the HTTP method to be used, defaults to `GET`. :type method: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' # ensure we've set the request_token_url if self.request_token_url is None: raise TypeError('request_token_url must not be None') session = self.get_session() self.request_token_response = session.request(method, self.request_token_url, **kwargs) return self.request_token_response def get_request_token(self, method='GET', decoder=parse_utf8_qsl, key_token='oauth_token', key_token_secret='oauth_token_secret', **kwargs): ''' Return a request token pair. :param method: A string representation of the HTTP method to be used, defaults to `GET`. :type method: str :param decoder: A function used to parse the Response content. Should return a dictionary. :type decoder: func :param key_token: The key the access token will be decoded by, defaults to 'oauth_token'. :type string: :param key_token_secret: The key the access token will be decoded by, defaults to 'oauth_token_secret'. :type string: :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' r = self.get_raw_request_token(method=method, **kwargs) request_token, request_token_secret = \ process_token_request(r, decoder, key_token, key_token_secret) return request_token, request_token_secret def get_authorize_url(self, request_token, **params): ''' Returns a formatted authorize URL. :param request_token: The request token as returned by :class:`get_request_token`. :type request_token: str :param \*\*params: Additional keyworded arguments to be added to the request querystring. :type \*\*params: dict ''' params.update({'oauth_token': request_token}) return self.authorize_url + '?' + urlencode(params) def get_raw_access_token(self, request_token, request_token_secret, method='GET', **kwargs): ''' Returns a Requests' response over the :attr:`rauth.OAuth1Service.access_token_url`. Use this if your endpoint if you need the full `Response` object. :param request_token: The request token as returned by :meth:`get_request_token`. :type request_token: str :param request_token_secret: The request token secret as returned by :meth:`get_request_token`. :type request_token_secret: str :param method: A string representation of the HTTP method to be used, defaults to `GET`. :type method: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' # ensure we've set the access_token_url if self.access_token_url is None: raise TypeError('access_token_url must not be None') session = self.get_session((request_token, request_token_secret)) self.access_token_response = session.request(method, self.access_token_url, **kwargs) return self.access_token_response def get_access_token(self, request_token, request_token_secret, method='GET', decoder=parse_utf8_qsl, key_token='oauth_token', key_token_secret='oauth_token_secret', **kwargs): ''' Returns an access token pair. :param request_token: The request token as returned by :meth:`get_request_token`. :type request_token: str :param request_token_secret: The request token secret as returned by :meth:`get_request_token`. :type request_token_secret: str :param method: A string representation of the HTTP method to be used, defaults to `GET`. :type method: str :param decoder: A function used to parse the Response content. Should return a dictionary. :type decoder: func :param key_token: The key the access token will be decoded by, defaults to 'oauth_token'. :type string: :param key_token_secret: The key the access token will be decoded by, defaults to 'oauth_token_secret'. :type string: :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' r = self.get_raw_access_token(request_token, request_token_secret, method=method, **kwargs) access_token, access_token_secret = \ process_token_request(r, decoder, key_token, key_token_secret) return access_token, access_token_secret def get_auth_session(self, request_token, request_token_secret, method='GET', **kwargs): ''' Gets an access token, intializes a new authenticated session with the access token. Returns an instance of :attr:`session_obj`. :param request_token: The request token as returned by :meth:`get_request_token`. :type request_token: str :param request_token_secret: The request token secret as returned by :meth:`get_request_token`. :type request_token_secret: str :param method: A string representation of the HTTP method to be used, defaults to `GET`. :type method: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' token = self.get_access_token(request_token, request_token_secret, method=method, **kwargs) session = self.get_session(token) if self.request_token_response: session.request_token_response = self.request_token_response if self.access_token_response: session.access_token_response = self.access_token_response return session class OAuth2Service(Service): ''' An OAuth 2.0 Service container. This class provides a wrapper around a specialized Requests' :class:`~requests.session.Session` object. Primarily this wrapper is used for producing authenticated session objects which are used to make requests against OAuth 2.0 endpoints. You might intialize :class:`OAuth2Service` something like this:: service = OAuth2Service( name='example', client_id='123', client_secret='456', access_token_url='https://example.com/token', authorize_url='https://example.com/authorize', base_url='https://example.com/api/') Given the simplicity of OAuth 2.0 now this object `service` can be used to retrieve an authenticated session in two simple steps:: # the return URL is used to validate the request params = {'redirect_uri': 'http://example.com/', 'response_type': 'code'} url = service.get_authorize_url(**params) # once the above URL is consumed by a client we can ask for an access # token. note that the code is retrieved from the redirect URL above, # as set by the provider data = {'code': 'foobar', 'grant_type': 'authorization_code', 'redirect_uri': 'http://example.com/'} session = service.get_auth_session(data=data) Now that we have retrieved a session, we may make requests against the OAuth 2.0 provider's endpoints. As much as possible the Requests' API is preserved and you may make requests using the same parameters you would using Requests:: r = session.get('foo', params={'format': 'json'}) print r.json() :param client_id: Client id. :type client_id: str :param client_secret: Client secret. :type client_secret: str :param name: The service name, defaults to `None`. :type name: str :param access_token_url: Access token endpoint, defaults to `None`. :type access_token_url: str :param authorize_url: Authorize endpoint, defaults to `None`. :type authorize_url: str :param base_url: A base URL from which to construct requests, defaults to `None`. :type base_url: str :param session_obj: Object used to construct sessions with, defaults to :class:`OAuth2Session` :type session_obj: :class:`rauth.Session` ''' __attrs__ = Service.__attrs__ + ['client_id', 'client_secret', 'access_token_url', 'session_obj'] def __init__(self, client_id, client_secret, name=None, access_token_url=None, authorize_url=None, base_url=None, session_obj=None): #: Client credentials. self.client_id = client_id self.client_secret = client_secret #: The provider's access token URL. self.access_token_url = access_token_url #: Object used to construct sessions with. self.session_obj = session_obj or OAuth2Session #: Access token response. self.access_token_response = None super(OAuth2Service, self).__init__(name, base_url, authorize_url) def get_session(self, token=None): ''' If provided, the `token` parameter is used to initialize an authenticated session, otherwise an unauthenticated session object is generated. Returns an instance of :attr:`session_obj`.. :param token: A token with which to initilize the session. :type token: str ''' if token is not None: session = self.session_obj(self.client_id, self.client_secret, token, service=self) else: # pragma: no cover session = self.session_obj(self.client_id, self.client_secret, service=self) return session def get_authorize_url(self, **params): ''' Returns a formatted authorize URL. :param \*\*params: Additional keyworded arguments to be added to the URL querystring. :type \*\*params: dict ''' params.update({'client_id': self.client_id}) return self.authorize_url + '?' + urlencode(params) def get_raw_access_token(self, method='POST', **kwargs): ''' Returns a Requests' response over the :attr:`OAuth2Service.access_token_url`. Use this if your endpoint if you need the full `Response` object. :param method: A string representation of the HTTP method to be used, defaults to `POST`. :type method: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' key = 'params' if method in ENTITY_METHODS: key = 'data' kwargs.setdefault(key, {}) kwargs[key].update({'client_id': self.client_id, 'client_secret': self.client_secret}) session = self.get_session() self.access_token_response = session.request(method, self.access_token_url, **kwargs) return self.access_token_response def get_access_token(self, method='POST', decoder=parse_utf8_qsl, key='access_token', **kwargs): ''' Returns an access token. :param method: A string representation of the HTTP method to be used, defaults to `POST`. :type method: str :param decoder: A function used to parse the Response content. Should return a dictionary. :type decoder: func :param key: The key the access token will be decoded by, defaults to 'access_token'. :type string: :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' r = self.get_raw_access_token(method, **kwargs) access_token, = process_token_request(r, decoder, key) return access_token def get_auth_session(self, method='POST', **kwargs): ''' Gets an access token, intializes a new authenticated session with the access token. Returns an instance of :attr:`session_obj`. :param method: A string representation of the HTTP method to be used, defaults to `POST`. :type method: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' session = self.get_session(self.get_access_token(method, **kwargs)) if self.access_token_response: session.access_token_response = self.access_token_response return session class OflyService(Service): ''' An Ofly Service container. This class wraps an Ofly service i.e., Shutterfly. The process is similar to that of OAuth 1.0 but simplified. You might intialize :class:`OflyService` something like this:: service = OflyService(name='example', app_id='123', app_secret='456', authorize_url='http://example.com/authorize') A signed authorize URL is then produced via calling `service.get_authorize_url`. Once this has been visited by the client and assuming the client authorizes the request. Normal API calls can now be made using a session instance. Retrieve the authenticated session like so:: session = service.get_auth_session('foo') # now we can make regular Requests' calls r = session.get('bar') :param app_id: The oFlyAppId, i.e. "application ID". :type app_id: str :param app_secret: The oFlyAppSecret, i.e. "shared secret". :type app_secret: str :param name: The service name, defaults to `None`. :type name: str :param authorize_url: Authorize endpoint, defaults to `None`. :type authorize_url: str :param base_url: A base URL from which to construct requests, defaults to `None`. :type base_url: str :param user_id: The oflyUserid, defaults to `None`. Note: this is required for Ofly requests, retrieved via authorize URL. :type user_id: str :param session_obj: Object used to construct sessions with, defaults to `rauth.OflySession` :type session_obj: :class:`rauth.Session` ''' __attrs__ = Service.__attrs__ + ['app_id', 'app_secret', 'user_id', 'session_obj'] def __init__(self, app_id, app_secret, name=None, authorize_url=None, base_url=None, user_id=None, session_obj=None): #: Client credentials. self.app_id = app_id self.app_secret = app_secret #: The oflyUserid. self.user_id = user_id #: Object used to construct sessions with. self.session_obj = session_obj or OflySession super(OflyService, self).__init__(name, base_url, authorize_url) def get_session(self, token): ''' The token parameter should be `oFlyUserid`. This is used to initialize an authenticated session instance. Returns an instance of :attr:`session_obj`. :param token: A token with which to initialize the session with, e.g. :attr:`OflyService.user_id`. :type token: str ''' return self.session_obj(self.app_id, self.app_secret, token, service=self) def get_authorize_url(self, **params): ''' Returns a formatted authorize URL. :param \*\*params: Additional keyworded arguments to be added to the request querystring. :type \*\*params: dict ''' params = self.session_obj.sign(self.authorize_url, self.app_id, self.app_secret, **params) return self.authorize_url + '?' + params def get_auth_session(self, user_id, **kwargs): ''' Intializes a new authenticated session with `user_id` as oFlyUserid. Returns an instance of :attr:`session_obj`. :param user_id: The oflyUserid, defaults to `None`. :type user_id: str :param \*\*kwargs: Optional arguments. Same as Requests. :type \*\*kwargs: dict ''' return self.get_session(user_id)