added updated version of oauthlib
[twitter-api-cdsw] / oauthlib / oauth2 / rfc6749 / clients / base.py
1 # -*- coding: utf-8 -*-
2 """
3 oauthlib.oauth2.rfc6749
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
6 This module is an implementation of various logic needed
7 for consuming OAuth 2.0 RFC6749.
8 """
9 from __future__ import absolute_import, unicode_literals
10
11 import time
12
13 from oauthlib.common import generate_token
14 from oauthlib.oauth2.rfc6749 import tokens
15 from oauthlib.oauth2.rfc6749.parameters import parse_token_response
16 from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
17 from oauthlib.oauth2.rfc6749.parameters import prepare_token_revocation_request
18 from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
19 from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
20 from oauthlib.oauth2.rfc6749.utils import is_secure_transport
21
22
23 AUTH_HEADER = 'auth_header'
24 URI_QUERY = 'query'
25 BODY = 'body'
26
27 FORM_ENC_HEADERS = {
28     'Content-Type': 'application/x-www-form-urlencoded'
29 }
30
31 class Client(object):
32
33     """Base OAuth2 client responsible for access token management.
34
35     This class also acts as a generic interface providing methods common to all
36     client types such as ``prepare_authorization_request`` and
37     ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
38     the recommended way of interacting with clients (as opposed to the abstract
39     prepare uri/body/etc methods). They are recommended over the older set
40     because they are easier to use (more consistent) and add a few additional
41     security checks, such as HTTPS and state checking.
42
43     Some of these methods require further implementation only provided by the
44     specific purpose clients such as
45     :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
46     seek to use the client class matching the OAuth workflow you need. For
47     Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
48
49     """
50
51     def __init__(self, client_id,
52                  default_token_placement=AUTH_HEADER,
53                  token_type='Bearer',
54                  access_token=None,
55                  refresh_token=None,
56                  mac_key=None,
57                  mac_algorithm=None,
58                  token=None,
59                  scope=None,
60                  state=None,
61                  redirect_url=None,
62                  state_generator=generate_token,
63                  **kwargs):
64         """Initialize a client with commonly used attributes.
65
66         :param client_id: Client identifier given by the OAuth provider upon
67         registration.
68
69         :param default_token_placement: Tokens can be supplied in the Authorization
70         header (default), the URL query component (``query``) or the request
71         body (``body``).
72
73         :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
74         if you specify the ``access_token`` parameter and know it is of a
75         different token type, such as a MAC, JWT or SAML token. Can
76         also be supplied as ``token_type`` inside the ``token`` dict parameter.
77
78         :param access_token: An access token (string) used to authenticate
79         requests to protected resources. Can also be supplied inside the
80         ``token`` dict parameter.
81
82         :param refresh_token: A refresh token (string) used to refresh expired
83         tokens. Can also be supplide inside the ``token`` dict parameter.
84
85         :param mac_key: Encryption key used with MAC tokens.
86
87         :param mac_algorithm:  Hashing algorithm for MAC tokens.
88
89         :param token: A dict of token attributes such as ``access_token``,
90         ``token_type`` and ``expires_at``.
91
92         :param scope: A list of default scopes to request authorization for.
93
94         :param state: A CSRF protection string used during authorization.
95
96         :param redirect_url: The redirection endpoint on the client side to which
97         the user returns after authorization.
98
99         :param state_generator: A no argument state generation callable. Defaults
100         to :py:meth:`oauthlib.common.generate_token`.
101         """
102
103         self.client_id = client_id
104         self.default_token_placement = default_token_placement
105         self.token_type = token_type
106         self.access_token = access_token
107         self.refresh_token = refresh_token
108         self.mac_key = mac_key
109         self.mac_algorithm = mac_algorithm
110         self.token = token or {}
111         self.scope = scope
112         self.state_generator = state_generator
113         self.state = state
114         self.redirect_url = redirect_url
115         self._expires_at = None
116         self._populate_attributes(self.token)
117
118     @property
119     def token_types(self):
120         """Supported token types and their respective methods
121
122         Additional tokens can be supported by extending this dictionary.
123
124         The Bearer token spec is stable and safe to use.
125
126         The MAC token spec is not yet stable and support for MAC tokens
127         is experimental and currently matching version 00 of the spec.
128         """
129         return {
130             'Bearer': self._add_bearer_token,
131             'MAC': self._add_mac_token
132         }
133
134     def prepare_request_uri(self, *args, **kwargs):
135         """Abstract method used to create request URIs."""
136         raise NotImplementedError("Must be implemented by inheriting classes.")
137
138     def prepare_request_body(self, *args, **kwargs):
139         """Abstract method used to create request bodies."""
140         raise NotImplementedError("Must be implemented by inheriting classes.")
141
142     def parse_request_uri_response(self, *args, **kwargs):
143         """Abstract method used to parse redirection responses."""
144
145     def add_token(self, uri, http_method='GET', body=None, headers=None,
146                   token_placement=None, **kwargs):
147         """Add token to the request uri, body or authorization header.
148
149         The access token type provides the client with the information
150         required to successfully utilize the access token to make a protected
151         resource request (along with type-specific attributes).  The client
152         MUST NOT use an access token if it does not understand the token
153         type.
154
155         For example, the "bearer" token type defined in
156         [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
157         token string in the request:
158
159         .. code-block:: http
160
161             GET /resource/1 HTTP/1.1
162             Host: example.com
163             Authorization: Bearer mF_9.B5f-4.1JqM
164
165         while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
166         utilized by issuing a MAC key together with the access token which is
167         used to sign certain components of the HTTP requests:
168
169         .. code-block:: http
170
171             GET /resource/1 HTTP/1.1
172             Host: example.com
173             Authorization: MAC id="h480djs93hd8",
174                                 nonce="274312:dj83hs9s",
175                                 mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
176
177         .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2
178         .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2
179         """
180         if not is_secure_transport(uri):
181             raise InsecureTransportError()
182
183         token_placement = token_placement or self.default_token_placement
184
185         case_insensitive_token_types = dict(
186             (k.lower(), v) for k, v in self.token_types.items())
187         if not self.token_type.lower() in case_insensitive_token_types:
188             raise ValueError("Unsupported token type: %s" % self.token_type)
189
190         if not self.access_token:
191             raise ValueError("Missing access token.")
192
193         if self._expires_at and self._expires_at < time.time():
194             raise TokenExpiredError()
195
196         return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
197                                                                      headers, token_placement, **kwargs)
198
199     def prepare_authorization_request(self, authorization_url, state=None,
200             redirect_url=None, scope=None, **kwargs):
201         """Prepare the authorization request.
202
203         This is the first step in many OAuth flows in which the user is
204         redirected to a certain authorization URL. This method adds
205         required parameters to the authorization URL.
206
207         :param authorization_url: Provider authorization endpoint URL.
208
209         :param state: CSRF protection string. Will be automatically created if
210         not provided. The generated state is available via the ``state``
211         attribute. Clients should verify that the state is unchanged and
212         present in the authorization response. This verification is done
213         automatically if using the ``authorization_response`` parameter
214         with ``prepare_token_request``.
215
216         :param redirect_url: Redirect URL to which the user will be returned
217         after authorization. Must be provided unless previously setup with
218         the provider. If provided then it must also be provided in the
219         token request.
220
221         :param kwargs: Additional parameters to included in the request.
222
223         :returns: The prepared request tuple with (url, headers, body).
224         """
225         if not is_secure_transport(authorization_url):
226             raise InsecureTransportError()
227
228         self.state = state or self.state_generator()
229         self.redirect_url = redirect_url or self.redirect_url
230         self.scope = scope or self.scope
231         auth_url = self.prepare_request_uri(
232                 authorization_url, redirect_uri=self.redirect_url,
233                 scope=self.scope, state=self.state, **kwargs)
234         return auth_url, FORM_ENC_HEADERS, ''
235
236     def prepare_token_request(self, token_url, authorization_response=None,
237             redirect_url=None, state=None, body='', **kwargs):
238         """Prepare a token creation request.
239
240         Note that these requests usually require client authentication, either
241         by including client_id or a set of provider specific authentication
242         credentials.
243
244         :param token_url: Provider token creation endpoint URL.
245
246         :param authorization_response: The full redirection URL string, i.e.
247         the location to which the user was redirected after successfull
248         authorization. Used to mine credentials needed to obtain a token
249         in this step, such as authorization code.
250
251         :param redirect_url: The redirect_url supplied with the authorization
252         request (if there was one).
253
254         :param body: Request body (URL encoded string).
255
256         :param kwargs: Additional parameters to included in the request.
257
258         :returns: The prepared request tuple with (url, headers, body).
259         """
260         if not is_secure_transport(token_url):
261             raise InsecureTransportError()
262
263         state = state or self.state
264         if authorization_response:
265             self.parse_request_uri_response(
266                     authorization_response, state=state)
267         self.redirect_url = redirect_url or self.redirect_url
268         body = self.prepare_request_body(body=body,
269                 redirect_uri=self.redirect_url, **kwargs)
270
271         return token_url, FORM_ENC_HEADERS, body
272
273     def prepare_refresh_token_request(self, token_url, refresh_token=None,
274             body='', scope=None, **kwargs):
275         """Prepare an access token refresh request.
276
277         Expired access tokens can be replaced by new access tokens without
278         going through the OAuth dance if the client obtained a refresh token.
279         This refresh token and authentication credentials can be used to
280         obtain a new access token, and possibly a new refresh token.
281
282         :param token_url: Provider token refresh endpoint URL.
283
284         :param refresh_token: Refresh token string.
285
286         :param body: Request body (URL encoded string).
287
288         :param scope: List of scopes to request. Must be equal to
289         or a subset of the scopes granted when obtaining the refresh
290         token.
291
292         :param kwargs: Additional parameters to included in the request.
293
294         :returns: The prepared request tuple with (url, headers, body).
295         """
296         if not is_secure_transport(token_url):
297             raise InsecureTransportError()
298
299         self.scope = scope or self.scope
300         body = self.prepare_refresh_body(body=body,
301                 refresh_token=refresh_token, scope=self.scope, **kwargs)
302         return token_url, FORM_ENC_HEADERS, body
303
304     def prepare_token_revocation_request(self, revocation_url, token,
305             token_type_hint="access_token", body='', callback=None, **kwargs):
306         """Prepare a token revocation request.
307
308         :param revocation_url: Provider token revocation endpoint URL.
309
310         :param token: The access or refresh token to be revoked (string).
311
312         :param token_type_hint: ``"access_token"`` (default) or
313         ``"refresh_token"``. This is optional and if you wish to not pass it you
314         must provide ``token_type_hint=None``.
315
316         :param callback: A jsonp callback such as ``package.callback`` to be invoked
317         upon receiving the response. Not that it should not include a () suffix.
318
319         :param kwargs: Additional parameters to included in the request.
320
321         :returns: The prepared request tuple with (url, headers, body).
322
323         Note that JSONP request may use GET requests as the parameters will
324         be added to the request URL query as opposed to the request body.
325
326         An example of a revocation request
327
328         .. code-block: http
329
330             POST /revoke HTTP/1.1
331             Host: server.example.com
332             Content-Type: application/x-www-form-urlencoded
333             Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
334
335             token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
336
337         An example of a jsonp revocation request
338
339         .. code-block: http
340
341             GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
342             Host: server.example.com
343             Content-Type: application/x-www-form-urlencoded
344             Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
345
346         and an error response
347
348         .. code-block: http
349
350         package.myCallback({"error":"unsupported_token_type"});
351
352         Note that these requests usually require client credentials, client_id in
353         the case for public clients and provider specific authentication
354         credentials for confidential clients.
355         """
356         if not is_secure_transport(revocation_url):
357             raise InsecureTransportError()
358
359         return prepare_token_revocation_request(revocation_url, token,
360                 token_type_hint=token_type_hint, body=body, callback=callback,
361                 **kwargs)
362
363     def parse_request_body_response(self, body, scope=None, **kwargs):
364         """Parse the JSON response body.
365
366         If the access token request is valid and authorized, the
367         authorization server issues an access token as described in
368         `Section 5.1`_.  A refresh token SHOULD NOT be included.  If the request
369         failed client authentication or is invalid, the authorization server
370         returns an error response as described in `Section 5.2`_.
371
372         :param body: The response body from the token request.
373         :param scope: Scopes originally requested.
374         :return: Dictionary of token parameters.
375         :raises: Warning if scope has changed. OAuth2Error if response is invalid.
376
377         These response are json encoded and could easily be parsed without
378         the assistance of OAuthLib. However, there are a few subtle issues
379         to be aware of regarding the response which are helpfully addressed
380         through the raising of various errors.
381
382         A successful response should always contain
383
384         **access_token**
385                 The access token issued by the authorization server. Often
386                 a random string.
387
388         **token_type**
389             The type of the token issued as described in `Section 7.1`_.
390             Commonly ``Bearer``.
391
392         While it is not mandated it is recommended that the provider include
393
394         **expires_in**
395             The lifetime in seconds of the access token.  For
396             example, the value "3600" denotes that the access token will
397             expire in one hour from the time the response was generated.
398             If omitted, the authorization server SHOULD provide the
399             expiration time via other means or document the default value.
400
401         **scope**
402             Providers may supply this in all responses but are required to only
403             if it has changed since the authorization request.
404
405         .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
406         .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
407         .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
408         """
409         self.token = parse_token_response(body, scope=scope)
410         self._populate_attributes(self.token)
411         return self.token
412
413     def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
414         """Prepare an access token request, using a refresh token.
415
416         If the authorization server issued a refresh token to the client, the
417         client makes a refresh request to the token endpoint by adding the
418         following parameters using the "application/x-www-form-urlencoded"
419         format in the HTTP request entity-body:
420
421         grant_type
422                 REQUIRED.  Value MUST be set to "refresh_token".
423         refresh_token
424                 REQUIRED.  The refresh token issued to the client.
425         scope
426                 OPTIONAL.  The scope of the access request as described by
427                 Section 3.3.  The requested scope MUST NOT include any scope
428                 not originally granted by the resource owner, and if omitted is
429                 treated as equal to the scope originally granted by the
430                 resource owner.
431         """
432         refresh_token = refresh_token or self.refresh_token
433         return prepare_token_request('refresh_token', body=body, scope=scope,
434                                      refresh_token=refresh_token, **kwargs)
435
436     def _add_bearer_token(self, uri, http_method='GET', body=None,
437                           headers=None, token_placement=None):
438         """Add a bearer token to the request uri, body or authorization header."""
439         if token_placement == AUTH_HEADER:
440             headers = tokens.prepare_bearer_headers(self.access_token, headers)
441
442         elif token_placement == URI_QUERY:
443             uri = tokens.prepare_bearer_uri(self.access_token, uri)
444
445         elif token_placement == BODY:
446             body = tokens.prepare_bearer_body(self.access_token, body)
447
448         else:
449             raise ValueError("Invalid token placement.")
450         return uri, headers, body
451
452     def _add_mac_token(self, uri, http_method='GET', body=None,
453                        headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
454         """Add a MAC token to the request authorization header.
455
456         Warning: MAC token support is experimental as the spec is not yet stable.
457         """
458         headers = tokens.prepare_mac_header(self.access_token, uri,
459                                             self.mac_key, http_method, headers=headers, body=body, ext=ext,
460                                             hash_algorithm=self.mac_algorithm, **kwargs)
461         return uri, headers, body
462
463     def _populate_attributes(self, response):
464         """Add commonly used values such as access_token to self."""
465
466         if 'access_token' in response:
467             self.access_token = response.get('access_token')
468
469         if 'refresh_token' in response:
470             self.refresh_token = response.get('refresh_token')
471
472         if 'token_type' in response:
473             self.token_type = response.get('token_type')
474
475         if 'expires_in' in response:
476             self.expires_in = response.get('expires_in')
477             self._expires_at = time.time() + int(self.expires_in)
478
479         if 'expires_at' in response:
480             self._expires_at = int(response.get('expires_at'))
481
482         if 'code' in response:
483             self.code = response.get('code')
484
485         if 'mac_key' in response:
486             self.mac_key = response.get('mac_key')
487
488         if 'mac_algorithm' in response:
489             self.mac_algorithm = response.get('mac_algorithm')
490

Benjamin Mako Hill || Want to submit a patch?