]> projects.mako.cc - twitter-api-cdsw/blob - requests_oauthlib/oauth1_session.py
Handle content-type header charset value for streaming API
[twitter-api-cdsw] / requests_oauthlib / oauth1_session.py
1 from __future__ import unicode_literals
2
3 try:
4     from urlparse import urlparse
5 except ImportError:
6     from urllib.parse import urlparse
7
8 import logging
9
10 from oauthlib.common import add_params_to_uri
11 from oauthlib.common import urldecode as _urldecode
12 from oauthlib.oauth1 import (
13     SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER
14 )
15 import requests
16
17 from . import OAuth1
18
19 import sys
20 if sys.version > "3":
21     unicode = str
22
23
24 log = logging.getLogger(__name__)
25
26
27 def urldecode(body):
28     """Parse query or json to python dictionary"""
29     try:
30         return _urldecode(body)
31     except:
32         import json
33         return json.loads(body)
34
35
36 class TokenRequestDenied(ValueError):
37
38     def __init__(self, message, status_code):
39         super(TokenRequestDenied, self).__init__(message)
40         self.status_code = status_code
41
42
43 class TokenMissing(ValueError):
44     def __init__(self, message, response):
45         super(TokenMissing, self).__init__(message)
46         self.response = response
47
48
49 class VerifierMissing(ValueError):
50     pass
51
52
53 class OAuth1Session(requests.Session):
54     """Request signing and convenience methods for the oauth dance.
55
56     What is the difference between OAuth1Session and OAuth1?
57
58     OAuth1Session actually uses OAuth1 internally and its purpose is to assist
59     in the OAuth workflow through convenience methods to prepare authorization
60     URLs and parse the various token and redirection responses. It also provide
61     rudimentary validation of responses.
62
63     An example of the OAuth workflow using a basic CLI app and Twitter.
64
65     >>> # Credentials obtained during the registration.
66     >>> client_key = 'client key'
67     >>> client_secret = 'secret'
68     >>> callback_uri = 'https://127.0.0.1/callback'
69     >>>
70     >>> # Endpoints found in the OAuth provider API documentation
71     >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
72     >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
73     >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
74     >>>
75     >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri)
76     >>>
77     >>> # First step, fetch the request token.
78     >>> oauth_session.fetch_request_token(request_token_url)
79     {
80         'oauth_token': 'kjerht2309u',
81         'oauth_token_secret': 'lsdajfh923874',
82     }
83     >>>
84     >>> # Second step. Follow this link and authorize
85     >>> oauth_session.authorization_url(authorization_url)
86     'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
87     >>>
88     >>> # Third step. Fetch the access token
89     >>> redirect_response = raw_input('Paste the full redirect URL here.')
90     >>> oauth_session.parse_authorization_response(redirect_response)
91     {
92         'oauth_token: 'kjerht2309u',
93         'oauth_token_secret: 'lsdajfh923874',
94         'oauth_verifier: 'w34o8967345',
95     }
96     >>> oauth_session.fetch_access_token(access_token_url)
97     {
98         'oauth_token': 'sdf0o9823sjdfsdf',
99         'oauth_token_secret': '2kjshdfp92i34asdasd',
100     }
101     >>> # Done. You can now make OAuth requests.
102     >>> status_url = 'http://api.twitter.com/1/statuses/update.json'
103     >>> new_status = {'status':  'hello world!'}
104     >>> oauth_session.post(status_url, data=new_status)
105     <Response [200]>
106     """
107
108     def __init__(self, client_key,
109             client_secret=None,
110             resource_owner_key=None,
111             resource_owner_secret=None,
112             callback_uri=None,
113             signature_method=SIGNATURE_HMAC,
114             signature_type=SIGNATURE_TYPE_AUTH_HEADER,
115             rsa_key=None,
116             verifier=None,
117             client_class=None,
118             force_include_body=False,
119             **kwargs):
120         """Construct the OAuth 1 session.
121
122         :param client_key: A client specific identifier.
123         :param client_secret: A client specific secret used to create HMAC and
124                               plaintext signatures.
125         :param resource_owner_key: A resource owner key, also referred to as
126                                    request token or access token depending on
127                                    when in the workflow it is used.
128         :param resource_owner_secret: A resource owner secret obtained with
129                                       either a request or access token. Often
130                                       referred to as token secret.
131         :param callback_uri: The URL the user is redirect back to after
132                              authorization.
133         :param signature_method: Signature methods determine how the OAuth
134                                  signature is created. The three options are
135                                  oauthlib.oauth1.SIGNATURE_HMAC (default),
136                                  oauthlib.oauth1.SIGNATURE_RSA and
137                                  oauthlib.oauth1.SIGNATURE_PLAIN.
138         :param signature_type: Signature type decides where the OAuth
139                                parameters are added. Either in the
140                                Authorization header (default) or to the URL
141                                query parameters or the request body. Defined as
142                                oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER,
143                                oauthlib.oauth1.SIGNATURE_TYPE_QUERY and
144                                oauthlib.oauth1.SIGNATURE_TYPE_BODY
145                                respectively.
146         :param rsa_key: The private RSA key as a string. Can only be used with
147                         signature_method=oauthlib.oauth1.SIGNATURE_RSA.
148         :param verifier: A verifier string to prove authorization was granted.
149         :param client_class: A subclass of `oauthlib.oauth1.Client` to use with
150                              `requests_oauthlib.OAuth1` instead of the default
151         :param force_include_body: Always include the request body in the
152                                    signature creation.
153         :param **kwargs: Additional keyword arguments passed to `OAuth1`
154         """
155         super(OAuth1Session, self).__init__()
156         self._client = OAuth1(client_key,
157                 client_secret=client_secret,
158                 resource_owner_key=resource_owner_key,
159                 resource_owner_secret=resource_owner_secret,
160                 callback_uri=callback_uri,
161                 signature_method=signature_method,
162                 signature_type=signature_type,
163                 rsa_key=rsa_key,
164                 verifier=verifier,
165                 client_class=client_class,
166                 force_include_body=force_include_body,
167                 **kwargs)
168         self.auth = self._client
169
170     @property
171     def authorized(self):
172         """Boolean that indicates whether this session has an OAuth token
173         or not. If `self.authorized` is True, you can reasonably expect
174         OAuth-protected requests to the resource to succeed. If
175         `self.authorized` is False, you need the user to go through the OAuth
176         authentication dance before OAuth-protected requests to the resource
177         will succeed.
178         """
179         if self._client.client.signature_method == SIGNATURE_RSA:
180             # RSA only uses resource_owner_key
181             return bool(self._client.client.resource_owner_key)
182         else:
183             # other methods of authentication use all three pieces
184             return (
185                 bool(self._client.client.client_secret) and
186                 bool(self._client.client.resource_owner_key) and
187                 bool(self._client.client.resource_owner_secret)
188             )
189
190     def authorization_url(self, url, request_token=None, **kwargs):
191         """Create an authorization URL by appending request_token and optional
192         kwargs to url.
193
194         This is the second step in the OAuth 1 workflow. The user should be
195         redirected to this authorization URL, grant access to you, and then
196         be redirected back to you. The redirection back can either be specified
197         during client registration or by supplying a callback URI per request.
198
199         :param url: The authorization endpoint URL.
200         :param request_token: The previously obtained request token.
201         :param kwargs: Optional parameters to append to the URL.
202         :returns: The authorization URL with new parameters embedded.
203
204         An example using a registered default callback URI.
205
206         >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
207         >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
208         >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
209         >>> oauth_session.fetch_request_token(request_token_url)
210         {
211             'oauth_token': 'sdf0o9823sjdfsdf',
212             'oauth_token_secret': '2kjshdfp92i34asdasd',
213         }
214         >>> oauth_session.authorization_url(authorization_url)
215         'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf'
216         >>> oauth_session.authorization_url(authorization_url, foo='bar')
217         'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar'
218
219         An example using an explicit callback URI.
220
221         >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
222         >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
223         >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback')
224         >>> oauth_session.fetch_request_token(request_token_url)
225         {
226             'oauth_token': 'sdf0o9823sjdfsdf',
227             'oauth_token_secret': '2kjshdfp92i34asdasd',
228         }
229         >>> oauth_session.authorization_url(authorization_url)
230         'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
231         """
232         kwargs['oauth_token'] = request_token or self._client.client.resource_owner_key
233         log.debug('Adding parameters %s to url %s', kwargs, url)
234         return add_params_to_uri(url, kwargs.items())
235
236     def fetch_request_token(self, url, realm=None):
237         """Fetch a request token.
238
239         This is the first step in the OAuth 1 workflow. A request token is
240         obtained by making a signed post request to url. The token is then
241         parsed from the application/x-www-form-urlencoded response and ready
242         to be used to construct an authorization url.
243
244         :param url: The request token endpoint URL.
245         :param realm: A list of realms to request access to.
246         :returns: The response in dict format.
247
248         Note that a previously set callback_uri will be reset for your
249         convenience, or else signature creation will be incorrect on
250         consecutive requests.
251
252         >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
253         >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
254         >>> oauth_session.fetch_request_token(request_token_url)
255         {
256             'oauth_token': 'sdf0o9823sjdfsdf',
257             'oauth_token_secret': '2kjshdfp92i34asdasd',
258         }
259         """
260         self._client.client.realm = ' '.join(realm) if realm else None
261         token = self._fetch_token(url)
262         log.debug('Resetting callback_uri and realm (not needed in next phase).')
263         self._client.client.callback_uri = None
264         self._client.client.realm = None
265         return token
266
267     def fetch_access_token(self, url, verifier=None):
268         """Fetch an access token.
269
270         This is the final step in the OAuth 1 workflow. An access token is
271         obtained using all previously obtained credentials, including the
272         verifier from the authorization step.
273
274         Note that a previously set verifier will be reset for your
275         convenience, or else signature creation will be incorrect on
276         consecutive requests.
277
278         >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
279         >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
280         >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
281         >>> oauth_session.parse_authorization_response(redirect_response)
282         {
283             'oauth_token: 'kjerht2309u',
284             'oauth_token_secret: 'lsdajfh923874',
285             'oauth_verifier: 'w34o8967345',
286         }
287         >>> oauth_session.fetch_access_token(access_token_url)
288         {
289             'oauth_token': 'sdf0o9823sjdfsdf',
290             'oauth_token_secret': '2kjshdfp92i34asdasd',
291         }
292         """
293         if verifier:
294             self._client.client.verifier = verifier
295         if not getattr(self._client.client, 'verifier', None):
296             raise VerifierMissing('No client verifier has been set.')
297         token = self._fetch_token(url)
298         log.debug('Resetting verifier attribute, should not be used anymore.')
299         self._client.client.verifier = None
300         return token
301
302     def parse_authorization_response(self, url):
303         """Extract parameters from the post authorization redirect response URL.
304
305         :param url: The full URL that resulted from the user being redirected
306                     back from the OAuth provider to you, the client.
307         :returns: A dict of parameters extracted from the URL.
308
309         >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
310         >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
311         >>> oauth_session.parse_authorization_response(redirect_response)
312         {
313             'oauth_token: 'kjerht2309u',
314             'oauth_token_secret: 'lsdajfh923874',
315             'oauth_verifier: 'w34o8967345',
316         }
317         """
318         log.debug('Parsing token from query part of url %s', url)
319         token = dict(urldecode(urlparse(url).query))
320         log.debug('Updating internal client token attribute.')
321         self._populate_attributes(token)
322         return token
323
324     def _populate_attributes(self, token):
325         if 'oauth_token' in token:
326             self._client.client.resource_owner_key = token['oauth_token']
327         else:
328             raise TokenMissing(
329                 'Response does not contain a token: {resp}'.format(resp=token),
330                 token,
331             )
332         if 'oauth_token_secret' in token:
333             self._client.client.resource_owner_secret = (
334                 token['oauth_token_secret'])
335         if 'oauth_verifier' in token:
336             self._client.client.verifier = token['oauth_verifier']
337
338     def _fetch_token(self, url):
339         log.debug('Fetching token from %s using client %s', url, self._client.client)
340         r = self.post(url)
341
342         if r.status_code >= 400:
343             error = "Token request failed with code %s, response was '%s'."
344             raise TokenRequestDenied(error % (r.status_code, r.text), r.status_code)
345
346         log.debug('Decoding token from response "%s"', r.text)
347         try:
348             token = dict(urldecode(r.text))
349         except ValueError as e:
350             error = ("Unable to decode token from token response. "
351                      "This is commonly caused by an unsuccessful request where"
352                      " a non urlencoded error message is returned. "
353                      "The decoding error was %s""" % e)
354             raise ValueError(error)
355
356         log.debug('Obtained token %s', token)
357         log.debug('Updating internal client attributes from token data.')
358         self._populate_attributes(token)
359         return token
360
361     def rebuild_auth(self, prepared_request, response):
362         """
363         When being redirected we should always strip Authorization
364         header, since nonce may not be reused as per OAuth spec.
365         """
366         if 'Authorization' in prepared_request.headers:
367             # If we get redirected to a new host, we should strip out
368             # any authentication headers.
369             prepared_request.headers.pop('Authorization', True)
370             prepared_request.prepare_auth(self.auth)
371         return

Benjamin Mako Hill || Want to submit a patch?