From 52028dfd9d1930d7c6e2fac4777c4b2c2161cc86 Mon Sep 17 00:00:00 2001 From: Benjamin Mako Hill Date: Fri, 1 May 2015 11:23:17 -0700 Subject: [PATCH] import requests_oauthlib (a dependency) --- docs/requests-oauthlib-AUTHORS.rst | 23 ++ docs/requests-oauthlib-HISTORY.rst | 14 + docs/requests-oauthlib-LICENSE | 13 + docs/requests-oauthlib-README.rst | 57 ++++ requests_oauthlib/__init__.py | 6 + .../compliance_fixes/__init__.py | 4 + .../compliance_fixes/facebook.py | 17 ++ .../compliance_fixes/linkedin.py | 24 ++ requests_oauthlib/oauth1_auth.py | 71 +++++ requests_oauthlib/oauth1_session.py | 266 +++++++++++++++++ requests_oauthlib/oauth2_auth.py | 36 +++ requests_oauthlib/oauth2_session.py | 273 ++++++++++++++++++ requests_oauthlib/utils.py | 9 + 13 files changed, 813 insertions(+) create mode 100644 docs/requests-oauthlib-AUTHORS.rst create mode 100644 docs/requests-oauthlib-HISTORY.rst create mode 100644 docs/requests-oauthlib-LICENSE create mode 100644 docs/requests-oauthlib-README.rst create mode 100644 requests_oauthlib/__init__.py create mode 100644 requests_oauthlib/compliance_fixes/__init__.py create mode 100644 requests_oauthlib/compliance_fixes/facebook.py create mode 100644 requests_oauthlib/compliance_fixes/linkedin.py create mode 100644 requests_oauthlib/oauth1_auth.py create mode 100644 requests_oauthlib/oauth1_session.py create mode 100644 requests_oauthlib/oauth2_auth.py create mode 100644 requests_oauthlib/oauth2_session.py create mode 100644 requests_oauthlib/utils.py diff --git a/docs/requests-oauthlib-AUTHORS.rst b/docs/requests-oauthlib-AUTHORS.rst new file mode 100644 index 0000000..fb522a3 --- /dev/null +++ b/docs/requests-oauthlib-AUTHORS.rst @@ -0,0 +1,23 @@ +Requests-oauthlib is written and maintained by Kenneth Reitz and various +contributors: + +Development Lead +---------------- + +- Kenneth Reitz + +Patches and Suggestions +----------------------- + +- Cory Benfield +- Ib Lundgren +- Devin Sevilla +- Imad Mouhtassem +- Johan Euphrosine +- Johannes Spielmann +- Martin Trigaux +- Matt McClure +- Mikhail Sobolev +- Paul Bonser +- Vinay Raikar +- kracekumar diff --git a/docs/requests-oauthlib-HISTORY.rst b/docs/requests-oauthlib-HISTORY.rst new file mode 100644 index 0000000..3cc6cca --- /dev/null +++ b/docs/requests-oauthlib-HISTORY.rst @@ -0,0 +1,14 @@ +History +------- + +v0.4.0 (29 September 2013) +++++++++++++++++++++++++++ +- OAuth1Session methods only return unicode strings. #55. +- Renamed requests_oauthlib.core to requests_oauthlib.oauth1_auth for consistency. #79. +- Added Facebook compliance fix and access_token_response hook to OAuth2Session. #63. +- Added LinkedIn compliance fix. +- Added refresh_token_response compliance hook, invoked before parsing the refresh token. +- Correctly limit compliance hooks to running only once! +- Content type guessing should only be done when no content type is given +- OAuth1 now updates r.headers instead of replacing it with non case insensitive dict +- Remove last use of Response.content (in OAuth1Session). #44. diff --git a/docs/requests-oauthlib-LICENSE b/docs/requests-oauthlib-LICENSE new file mode 100644 index 0000000..5069506 --- /dev/null +++ b/docs/requests-oauthlib-LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013 Kenneth Reitz. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/docs/requests-oauthlib-README.rst b/docs/requests-oauthlib-README.rst new file mode 100644 index 0000000..0207e7d --- /dev/null +++ b/docs/requests-oauthlib-README.rst @@ -0,0 +1,57 @@ +Requests-OAuthlib +================= + +This project provides first-class OAuth library support for `Requests `_. + +The OAuth 1 workflow +-------------------- + +OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, +requests_oauthlib hides most of these and let you focus at the task at hand. + +Accessing protected resources using requests_oauthlib is as simple as: + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth1Session + >>> twitter = OAuth1Session('client_key', + client_secret='client_secret', + resource_owner_key='resource_owner_key', + resource_owner_secret='resource_owner_secret') + >>> url = 'https://api.twitter.com/1/account/settings.json' + >>> r = twitter.get(url) + +Before accessing resources you will need to obtain a few credentials from your +provider (i.e. Twitter) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 1 workflow guide on RTD `_. + +The OAuth 2 workflow +-------------------- + +OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most +common being the Authorization Code Grant, also known as the WebApplication +flow. + +Fetching a protected resource after obtaining an access token can be as simple as: + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth2Session + >>> google = OAuth2Session(r'client_id', token=r'token') + >>> url = 'https://www.googleapis.com/oauth2/v1/userinfo' + >>> r = google.get(url) + +Before accessing resources you will need to obtain a few credentials from your +provider (i.e. Google) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 2 workflow guide on RTD `_. + +Installation +------------- + +To install requests and requests_oauthlib you can use pip: + +.. code-block:: bash + + $ pip install requests requests_oauthlib diff --git a/requests_oauthlib/__init__.py b/requests_oauthlib/__init__.py new file mode 100644 index 0000000..a89f9ce --- /dev/null +++ b/requests_oauthlib/__init__.py @@ -0,0 +1,6 @@ +from .oauth1_auth import OAuth1 +from .oauth1_session import OAuth1Session +from .oauth2_auth import OAuth2 +from .oauth2_session import OAuth2Session, TokenUpdated + +__version__ = '0.4.0' diff --git a/requests_oauthlib/compliance_fixes/__init__.py b/requests_oauthlib/compliance_fixes/__init__.py new file mode 100644 index 0000000..e828a27 --- /dev/null +++ b/requests_oauthlib/compliance_fixes/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import + +from .facebook import facebook_compliance_fix +from .linkedin import linkedin_compliance_fix diff --git a/requests_oauthlib/compliance_fixes/facebook.py b/requests_oauthlib/compliance_fixes/facebook.py new file mode 100644 index 0000000..c3c3012 --- /dev/null +++ b/requests_oauthlib/compliance_fixes/facebook.py @@ -0,0 +1,17 @@ +from json import dumps +from oauthlib.common import urldecode + + +def facebook_compliance_fix(session): + + def _compliance_fix(r): + token = dict(urldecode(r.text)) + expires = token.get('expires') + if expires is not None: + token['expires_in'] = expires + token['token_type'] = 'Bearer' + r._content = dumps(token) + return r + + session.register_compliance_hook('access_token_response', _compliance_fix) + return session diff --git a/requests_oauthlib/compliance_fixes/linkedin.py b/requests_oauthlib/compliance_fixes/linkedin.py new file mode 100644 index 0000000..ad19004 --- /dev/null +++ b/requests_oauthlib/compliance_fixes/linkedin.py @@ -0,0 +1,24 @@ +from json import loads, dumps + +from oauthlib.common import add_params_to_uri + + +def linkedin_compliance_fix(session): + + def _missing_token_type(r): + token = loads(r.text) + token['token_type'] = 'Bearer' + r._content = dumps(token) + return r + + def _non_compliant_param_name(url, headers, data): + token = [('oauth2_access_token', session._client.access_token)] + url = add_params_to_uri(url, token) + return url, headers, data + + session._client.default_token_placement = 'query' + session.register_compliance_hook('access_token_response', + _missing_token_type) + session.register_compliance_hook('protected_request', + _non_compliant_param_name) + return session diff --git a/requests_oauthlib/oauth1_auth.py b/requests_oauthlib/oauth1_auth.py new file mode 100644 index 0000000..e38a889 --- /dev/null +++ b/requests_oauthlib/oauth1_auth.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from oauthlib.common import extract_params +from oauthlib.oauth1 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER) + +CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' +CONTENT_TYPE_MULTI_PART = 'multipart/form-data' + +import sys +if sys.version > "3": + unicode = str + + def to_native_str(string): + return string.decode('utf-8') +else: + def to_native_str(string): + return string + +# OBS!: Correct signing of requests are conditional on invoking OAuth1 +# as the last step of preparing a request, or at least having the +# content-type set properly. +class OAuth1(object): + """Signs the request using OAuth 1 (RFC5849)""" + def __init__(self, client_key, + client_secret=None, + resource_owner_key=None, + resource_owner_secret=None, + callback_uri=None, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_AUTH_HEADER, + rsa_key=None, verifier=None, + decoding='utf-8'): + + try: + signature_type = signature_type.upper() + except AttributeError: + pass + + self.client = Client(client_key, client_secret, resource_owner_key, + resource_owner_secret, callback_uri, signature_method, + signature_type, rsa_key, verifier, decoding=decoding) + + def __call__(self, r): + """Add OAuth parameters to the request. + + Parameters may be included from the body if the content-type is + urlencoded, if no content type is set a guess is made. + """ + # Overwriting url is safe here as request will not modify it past + # this point. + + content_type = r.headers.get('Content-Type', '') + if not content_type and extract_params(r.body): + content_type = CONTENT_TYPE_FORM_URLENCODED + if not isinstance(content_type, unicode): + content_type = content_type.decode('utf-8') + + is_form_encoded = (CONTENT_TYPE_FORM_URLENCODED in content_type) + + if is_form_encoded: + r.headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED + r.url, headers, r.body = self.client.sign( + unicode(r.url), unicode(r.method), r.body or '', r.headers) + else: + # Omit body data in the signing of non form-encoded requests + r.url, headers, _ = self.client.sign( + unicode(r.url), unicode(r.method), None, r.headers) + + r.prepare_headers(headers) + r.url = to_native_str(r.url) + return r diff --git a/requests_oauthlib/oauth1_session.py b/requests_oauthlib/oauth1_session.py new file mode 100644 index 0000000..cadb09d --- /dev/null +++ b/requests_oauthlib/oauth1_session.py @@ -0,0 +1,266 @@ +from __future__ import unicode_literals + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse + +from oauthlib.common import add_params_to_uri, urldecode +from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER +import requests + +from . import OAuth1 + +import sys +if sys.version > "3": + unicode = str + + +class OAuth1Session(requests.Session): + """Request signing and convenience methods for the oauth dance. + + What is the difference between OAuth1Session and OAuth1? + + OAuth1Session actually uses OAuth1 internally and it's purpose is to assist + in the OAuth workflow through convenience methods to prepare authorization + URLs and parse the various token and redirection responses. It also provide + rudimentary validation of responses. + + An example of the OAuth workflow using a basic CLI app and Twitter. + + >>> # Credentials obtained during the registration. + >>> client_key = 'client key' + >>> client_secret = 'secret' + >>> callback_uri = 'https://127.0.0.1/callback' + >>> + >>> # Endpoints found in the OAuth provider API documentation + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> access_token_url = 'https://api.twitter.com/oauth/access_token' + >>> + >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri) + >>> + >>> # First step, fetch the request token. + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'kjerht2309u', + 'oauth_token_secret': 'lsdajfh923874', + } + >>> + >>> # Second step. Follow this link and authorize + >>> oauth_session.authorization_url(authorization_url) + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback' + >>> + >>> # Third step. Fetch the access token + >>> redirect_response = raw_input('Paste the full redirect URL here.') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + >>> oauth_session.fetch_access_token(access_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> # Done. You can now make OAuth requests. + >>> status_url = 'http://api.twitter.com/1/statuses/update.json' + >>> new_status = {'status': 'hello world!'} + >>> oauth_session.post(status_url, data=new_status) + + """ + + def __init__(self, client_key, + client_secret=None, + resource_owner_key=None, + resource_owner_secret=None, + callback_uri=None, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_AUTH_HEADER, + rsa_key=None, + verifier=None): + """Construct the OAuth 1 session. + + :param client_key: A client specific identifier. + :param client_secret: A client specific secret used to create HMAC and + plaintext signatures. + :param resource_owner_key: A resource owner key, also referred to as + request token or access token depending on + when in the workflow it is used. + :param resource_owner_secret: A resource owner secret obtained with + either a request or access token. Often + referred to as token secret. + :param callback_uri: The URL the user is redirect back to after + authorization. + :param signature_method: Signature methods determine how the OAuth + signature is created. The three options are + oauthlib.oauth1.SIGNATURE_HMAC (default), + oauthlib.oauth1.SIGNATURE_RSA and + oauthlib.oauth1.SIGNATURE_PLAIN. + :param signature_type: Signature type decides where the OAuth + parameters are added. Either in the + Authorization header (default) or to the URL + query parameters or the request body. Defined as + oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER, + oauthlib.oauth1.SIGNATURE_TYPE_QUERY and + oauthlib.oauth1.SIGNATURE_TYPE_BODY + respectively. + :param rsa_key: The private RSA key as a string. Can only be used with + signature_method=oauthlib.oauth1.SIGNATURE_RSA. + :param verifier: A verifier string to prove authorization was granted. + """ + super(OAuth1Session, self).__init__() + self._client = OAuth1(client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, + callback_uri=callback_uri, + signature_method=signature_method, + signature_type=signature_type, + rsa_key=rsa_key, + verifier=verifier) + self.auth = self._client + + def authorization_url(self, url, request_token=None, **kwargs): + """Create an authorization URL by appending request_token and optional + kwargs to url. + + This is the second step in the OAuth 1 workflow. The user should be + redirected to this authorization URL, grant access to you, and then + be redirected back to you. The redirection back can either be specified + during client registration or by supplying a callback URI per request. + + :param url: The authorization endpoint URL. + :param request_token: The previously obtained request token. + :param kwargs: Optional parameters to append to the URL. + :returns: The authorization URL with new parameters embedded. + + An example using a registered default callback URI. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> oauth_session.authorization_url(authorization_url) + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf' + >>> oauth_session.authorization_url(authorization_url, foo='bar') + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar' + + An example using an explicit callback URI. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> oauth_session.authorization_url(authorization_url) + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback' + """ + kwargs['oauth_token'] = request_token or self._client.client.resource_owner_key + return add_params_to_uri(url, kwargs.items()) + + def fetch_request_token(self, url, realm=None): + """Fetch a request token. + + This is the first step in the OAuth 1 workflow. A request token is + obtained by making a signed post request to url. The token is then + parsed from the application/x-www-form-urlencoded response and ready + to be used to construct an authorization url. + + :param url: The request token endpoint URL. + :param realm: A list of realms to request access to. + :returns: The response in dict format. + + Note that a previously set callback_uri will be reset for your + convenience, or else signature creation will be incorrect on + consecutive requests. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + """ + self._client.client.realm = ' '.join(realm) if realm else None + token = self._fetch_token(url) + self._client.client.callback_uri = None + self._client.client.realm = None + return token + + def fetch_access_token(self, url): + """Fetch an access token. + + This is the final step in the OAuth 1 workflow. An access token is + obtained using all previously obtained credentials, including the + verifier from the authorization step. + + Note that a previously set verifier will be reset for your + convenience, or else signature creation will be incorrect on + consecutive requests. + + >>> access_token_url = 'https://api.twitter.com/oauth/access_token' + >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + >>> oauth_session.fetch_access_token(access_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + """ + if not hasattr(self._client.client, 'verifier'): + raise ValueError('No client verifier has been set.') + token = self._fetch_token(url) + self._client.client.verifier = None + return token + + def parse_authorization_response(self, url): + """Extract parameters from the post authorization redirect response URL. + + :param url: The full URL that resulted from the user being redirected + back from the OAuth provider to you, the client. + :returns: A dict of parameters extracted from the URL. + + >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + """ + token = dict(urldecode(urlparse(url).query)) + self._populate_attributes(token) + return token + + def _populate_attributes(self, token): + if 'oauth_token' in token: + self._client.client.resource_owner_key = token['oauth_token'] + else: + raise ValueError('Response does not contain a token. %s', token) + if 'oauth_token_secret' in token: + self._client.client.resource_owner_secret = ( + token['oauth_token_secret']) + if 'oauth_verifier' in token: + self._client.client.verifier = token['oauth_verifier'] + + def _fetch_token(self, url): + token = dict(urldecode(self.post(url).text)) + self._populate_attributes(token) + return token diff --git a/requests_oauthlib/oauth2_auth.py b/requests_oauthlib/oauth2_auth.py new file mode 100644 index 0000000..087b830 --- /dev/null +++ b/requests_oauthlib/oauth2_auth.py @@ -0,0 +1,36 @@ +from __future__ import unicode_literals +from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError + +from .utils import is_secure_transport + + +class OAuth2(object): + """Adds proof of authorization (OAuth2 token) to the request.""" + + def __init__(self, client_id=None, client=None, token=None): + """Construct a new OAuth 2 authorization object. + + :param client_id: Client id obtained during registration + :param client: :class:`oauthlib.oauth2.Client` to be used. Default is + WebApplicationClient which is useful for any + hosted application but not mobile or desktop. + :param token: Token dictionary, must include access_token + and token_type. + """ + self._client = client or WebApplicationClient(client_id, token=token) + if token: + for k, v in token.items(): + setattr(self._client, k, v) + + def __call__(self, r): + """Append an OAuth 2 token to the request. + + Note that currently HTTPS is required for all requests. There may be + a token type that allows for plain HTTP in the future and then this + should be updated to allow plain HTTP on a white list basis. + """ + if not is_secure_transport(r.url): + raise InsecureTransportError() + r.url, r.headers, r.body = self._client.add_token(r.url, + http_method=r.method, body=r.body, headers=r.headers) + return r diff --git a/requests_oauthlib/oauth2_session.py b/requests_oauthlib/oauth2_session.py new file mode 100644 index 0000000..beb199e --- /dev/null +++ b/requests_oauthlib/oauth2_session.py @@ -0,0 +1,273 @@ +from __future__ import unicode_literals +import os +import requests +from oauthlib.common import log, generate_token, urldecode +from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError +from oauthlib.oauth2 import TokenExpiredError + +from .utils import is_secure_transport + + +class TokenUpdated(Warning): + def __init__(self, token): + super(TokenUpdated, self).__init__() + self.token = token + + +class OAuth2Session(requests.Session): + """Versatile OAuth 2 extension to :class:`requests.Session`. + + Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec + including the four core OAuth 2 grants. + + Can be used to create authorization urls, fetch tokens and access protected + resources using the :class:`requests.Session` interface you are used to. + + - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant + - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant + - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant + - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant + + Note that the only time you will be using Implicit Grant from python is if + you are driving a user agent able to obtain URL fragments. + """ + + def __init__(self, client_id=None, client=None, auto_refresh_url=None, + auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None, + state=None, state_generator=None, token_updater=None, **kwargs): + """Construct a new OAuth 2 client session. + + :param client_id: Client id obtained during registration + :param client: :class:`oauthlib.oauth2.Client` to be used. Default is + WebApplicationClient which is useful for any + hosted application but not mobile or desktop. + :param scope: List of scopes you wish to request access to + :param redirect_uri: Redirect URI you registered as callback + :param token: Token dictionary, must include access_token + and token_type. + :param state: State string used to prevent CSRF. This will be given + when creating the authorization url and must be supplied + when parsing the authorization response. + Can be either a string or a no argument callable. + :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply + this if you wish the client to automatically refresh + your access tokens. + :auto_refresh_kwargs: Extra arguments to pass to the refresh token + endpoint. + :token_updater: Method with one argument, token, to be used to update + your token databse on automatic token refresh. If not + set a TokenUpdated warning will be raised when a token + has been refreshed. This warning will carry the token + in its token argument. + :param kwargs: Arguments to pass to the Session constructor. + """ + super(OAuth2Session, self).__init__(**kwargs) + self.client_id = client_id or client.client_id + self.scope = scope + self.redirect_uri = redirect_uri + self.token = token or {} + self.state = state or generate_token + self._state = None + self.auto_refresh_url = auto_refresh_url + self.auto_refresh_kwargs = auto_refresh_kwargs or {} + self.token_updater = token_updater + self._client = client or WebApplicationClient(client_id, token=token) + self._client._populate_attributes(token or {}) + + # Allow customizations for non compliant providers through various + # hooks to adjust requests and responses. + self.compliance_hook = { + 'access_token_response': set([]), + 'refresh_token_response': set([]), + 'protected_request': set([]), + } + + def new_state(self): + """Generates a state string to be used in authorizations.""" + try: + self._state = self.state() + log.debug('Generated new state %s.', self._state) + except TypeError: + self._state = self.state + log.debug('Re-using previously supplied state %s.', self._state) + return self._state + + def authorization_url(self, url, **kwargs): + """Form an authorization URL. + + :param url: Authorization endpoint url, must be HTTPS. + :param kwargs: Extra parameters to include. + :return: authorization_url, state + """ + state = self.new_state() + return self._client.prepare_request_uri(url, + redirect_uri=self.redirect_uri, + scope=self.scope, + state=state, + **kwargs), state + + def fetch_token(self, token_url, code=None, authorization_response=None, + body='', auth=None, username=None, password=None, **kwargs): + """Generic method for fetching an access token from the token endpoint. + + If you are using the MobileApplicationClient you will want to use + token_from_fragment instead of fetch_token. + + :param token_url: Token endpoint URL, must use HTTPS. + :param code: Authorization code (used by WebApplicationClients). + :param authorization_response: Authorization response URL, the callback + URL of the request back to you. Used by + WebApplicationClients instead of code. + :param body: Optional application/x-www-form-urlencoded body to add the + include in the token request. Prefer kwargs over body. + :param auth: An auth tuple or method as accepted by requests. + :param username: Username used by LegacyApplicationClients. + :param password: Password used by LegacyApplicationClients. + :param kwargs: Extra parameters to include in the token request. + :return: A token dict + """ + if not is_secure_transport(token_url): + raise InsecureTransportError() + + if not code and authorization_response: + self._client.parse_request_uri_response(authorization_response, + state=self._state) + code = self._client.code + elif not code and isinstance(self._client, WebApplicationClient): + code = self._client.code + if not code: + raise ValueError('Please supply either code or ' + 'authorization_code parameters.') + + + body = self._client.prepare_request_body(code=code, body=body, + redirect_uri=self.redirect_uri, username=username, + password=password, **kwargs) + # (ib-lundgren) All known, to me, token requests use POST. + r = self.post(token_url, data=dict(urldecode(body)), + headers={'Accept': 'application/json'}, auth=auth) + log.debug('Prepared fetch token request body %s', body) + log.debug('Request to fetch token completed with status %s.', + r.status_code) + log.debug('Response headers were %s and content %s.', + r.headers, r.text) + log.debug('Invoking %d token response hooks.', + len(self.compliance_hook['access_token_response'])) + for hook in self.compliance_hook['access_token_response']: + log.debug('Invoking hook %s.', hook) + r = hook(r) + + self._client.parse_request_body_response(r.text, scope=self.scope) + self.token = self._client.token + log.debug('Obtained token %s.', self.token) + return self.token + + def token_from_fragment(self, authorization_response): + """Parse token from the URI fragment, used by MobileApplicationClients. + + :param authorization_response: The full URL of the redirect back to you + :return: A token dict + """ + self._client.parse_request_uri_response(authorization_response, + state=self._state) + self.token = self._client.token + return self.token + + def refresh_token(self, token_url, refresh_token=None, body='', auth=None, + **kwargs): + """Fetch a new access token using a refresh token. + + :param token_url: The token endpoint, must be HTTPS. + :param refresh_token: The refresh_token to use. + :param body: Optional application/x-www-form-urlencoded body to add the + include in the token request. Prefer kwargs over body. + :param auth: An auth tuple or method as accepted by requests. + :param kwargs: Extra parameters to include in the token request. + :return: A token dict + """ + if not token_url: + raise ValueError('No token endpoint set for auto_refresh.') + + if not is_secure_transport(token_url): + raise InsecureTransportError() + + # Need to nullify token to prevent it from being added to the request + refresh_token = refresh_token or self.token.get('refresh_token') + self.token = {} + + log.debug('Adding auto refresh key word arguments %s.', + self.auto_refresh_kwargs) + kwargs.update(self.auto_refresh_kwargs) + body = self._client.prepare_refresh_body(body=body, + refresh_token=refresh_token, scope=self.scope, **kwargs) + log.debug('Prepared refresh token request body %s', body) + r = self.post(token_url, data=dict(urldecode(body)), auth=auth) + log.debug('Request to refresh token completed with status %s.', + r.status_code) + log.debug('Response headers were %s and content %s.', + r.headers, r.text) + log.debug('Invoking %d token response hooks.', + len(self.compliance_hook['refresh_token_response'])) + for hook in self.compliance_hook['refresh_token_response']: + log.debug('Invoking hook %s.', hook) + r = hook(r) + + self.token = self._client.parse_request_body_response(r.text, scope=self.scope) + if not 'refresh_token' in self.token: + log.debug('No new refresh token given. Re-using old.') + self.token['refresh_token'] = refresh_token + return self.token + + def request(self, method, url, data=None, headers=None, **kwargs): + """Intercept all requests and add the OAuth 2 token if present.""" + if not is_secure_transport(url): + raise InsecureTransportError() + if self.token: + log.debug('Invoking %d protected resource request hooks.', + len(self.compliance_hook['protected_request'])) + for hook in self.compliance_hook['protected_request']: + log.debug('Invoking hook %s.', hook) + url, headers, data = hook(url, headers, data) + + log.debug('Adding token %s to request.', self.token) + try: + url, headers, data = self._client.add_token(url, + http_method=method, body=data, headers=headers) + # Attempt to retrieve and save new access token if expired + except TokenExpiredError: + if self.auto_refresh_url: + log.debug('Auto refresh is set, attempting to refresh at %s.', + self.auto_refresh_url) + token = self.refresh_token(self.auto_refresh_url) + if self.token_updater: + log.debug('Updating token to %s using %s.', + token, self.token_updater) + self.token_updater(token) + url, headers, data = self._client.add_token(url, + http_method=method, body=data, headers=headers) + else: + raise TokenUpdated(token) + else: + raise + + log.debug('Requesting url %s using method %s.', url, method) + log.debug('Supplying headers %s and data %s', headers, data) + log.debug('Passing through key word arguments %s.', kwargs) + return super(OAuth2Session, self).request(method, url, + headers=headers, data=data, **kwargs) + + def register_compliance_hook(self, hook_type, hook): + """Register a hook for request/response tweaking. + + Available hooks are: + access_token_response invoked before token parsing. + refresh_token_response invoked before refresh token parsing. + protected_request invoked before making a request. + + If you find a new hook is needed please send a GitHub PR request + or open an issue. + """ + if hook_type not in self.compliance_hook: + raise ValueError('Hook type %s is not in %s.', + hook_type, self.compliance_hook) + self.compliance_hook[hook_type].add(hook) diff --git a/requests_oauthlib/utils.py b/requests_oauthlib/utils.py new file mode 100644 index 0000000..239e0d9 --- /dev/null +++ b/requests_oauthlib/utils.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals +import os + + +def is_secure_transport(uri): + """Check if the uri is over ssl.""" + if os.environ.get('DEBUG'): + return True + return uri.lower().startswith('https://') -- 2.30.2